Web Development

Things I have learnt in the first 5 minutes of using Elastic Search

After hearing all the raving about Elastic Search and how it was awesome and “rad” or whatever “hip-young” programmers want to say I decided I would give it a go.

To get the point since this might be a bit tl;dr: I am not overly fond of it. I am unsure what companies like GitHub see in it.

It has a queue, no need for a river

Excactly that, implement it into your active record and you don’t need to river.

I would in fact advise agains the river, it ues the oplog which can be slow and not only that but you are adding yet another lock on top of the secondaries that are trying to read as well, which may increase your chances of replication falling behind, this is of course dependant upon how often the river pings your oplog and how many new ops you have in that window.

This is a good point.

It has terrible documentation

Its documentation is great at explaining the API, no doubt about it but if you want to actually find out how something works and why something is then you have to constantly ask StackOverflow.

It just describes what parameters to put in and then leaves the rest upto you thinking that you don’t want to bother yourself with those details. We do though, we are not bandwagoning your product, we want to know how sharding and replication works, how indexes work and how to manage the product and more.

Even when looking at the API the documentation can sometimes be…unhelpful. Mainly due to its huge font-size, yet tiny middle centered layout, English language problems and disorganisation.

Overall I came out less than impressed about Elastic Searches documentation.

I actually Google search everything first so I don’t have to navigate that mess.

Lucene is not a good base

Yes Lucene is one of the originals when it comes to FTS techs but this isn’t a good thing. It was made back when people didn’t care about speed or scalability and all they cared about was that Lucene could Google.

This does notably mean that Lucene has many problems, like no infixing which translate to Elastic Search. This means at times to get the effect you want you must use prefix or wildcard searches which are so slow they are pretty much a death knell for any database, especially one which serves an FTS tech.

That is just one of the many problems that plagues modern Lucene, including a mix and match querying language from, the years of being backward compatible yet trying to keep up with changes.

It has some of the most verbose querying the universe

When querinyg a single keyword takes this much writing:

	    $cursor = glue::elasticSearch()->search(array('type' => 'help', 'body' =>
	        array('query' => array('filtered' => array(
	            'query' => array(
	                'bool' => array(
	                    'should' => array(
	                        array('multi_match' => array(
	                            'query' => glue::http()->param('query',$keywords), 
	                            'fields' => array('title', 'blurb', 'tags', 'normalisedTitle', 'path')
	                        )),
	                    )
	                )
	            )
	        )))
	    ));

And querying more than one takes:

        $res = glue::elasticSearch()->search(
                array('body' => array(
                        'query' => array(
                                'filtered' => array(
                                        'query' => array(
                                                'bool' => array(
                                                        'should' => array(
                                                                 
                                                                array('prefix' => array('username' => 'the')),

                                                                array('prefix' => array('username' => 'n')),
                                                                array('prefix' => array('username' => 'm')),
                                                                array('match' => array('about' => 'the')),
                                                        )
                                                )
                                        ),
                                        'filter' => array(
                                                'and' => array(
                                                        array('range' => array('created' => array('gte' => date('c',time()-3600), 'lte' => date('c',time()+3600))))
                                                )
                                        ),
                                        'sort' => array()
                                )
                        )
                )));

You do start to feel yourself slipping away.

You must do your own tokenizing if you wish to prefix on two keywords separately

Elastic Search won’t do this, it will actually search for a phrase by default even when you don’t use the phrase searcher.

It has some of the most complex querying the universe

When you have many ways to represent the same operator and 6 different operators for what could essentially be the same thing…

I believe this is legacy from Lucene, one of its many downsides for being old and backward compatible with every version.

It has no exact filtering without turning off the analyzer

Yep you read that right, you want to filter (yep filter not query) on deleted? Your gonna ahve to make sure it isn’t analyzed buddy.

It has no easy way to define indexes server side

I have no idea why elastic search does this but they make you define the indexes client side. In all my life I have never had a reason to do that and if you want to comment saying you do think about it carefully, “DO YOU REALLY???”.

Only about 1% of cases need client side index definition and the other 99% think they do.

Either way I am now stuck with having to have a elastic search setup script in my application about 500 lines long which has to be run in patches since you can’t run the delete index command and then the recreate index command, something about it being done sync and async.

It says to not use delete yet provides no feasible alternative

Its exact words are:

Note, delete by query bypasses versioning support. Also, it is not recommended to delete “large chunks of the data in an index”, many times, it’s better to simply reindex into a new index.

http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete-by-query.html

So if you need to delete a users videos from the videos type index then think again and since you have just shy of 200m records you can’t simply reindex.

Creating mappings is painful

No default mapping ability which means similarities between types are duplicated, 3/4 types have duplicated mappings.

EDIT: There is a default mapping is just their documentationis so terrible it was hidden under “dynamic mapping”: http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-dynamic-mapping.html

It is actually quite slow

Sphinx did it in about 1ms or less elastic search gets no results in 6ms.

Its querying is not very well standardised to other techs

from = skip/offset
size = limit

Seeing the problem yet?

Its PHP API is larger than my application in both file count and size

It is 5.42MB in size!!! To compare Yii is 58MB. That may not sound a lot but Yii is a full stack web framework…yep.

Its PHP API only takes composer

NOT EVERYONE USES COMPOSER!!!

The PHP API uses 9MB (base) per request

Under Sphinx that was 3.25MB

Decent AWS usage is an optional extra

Welcome to hell kid: http://www.elasticsearch.org/tutorials/elasticsearch-on-ec2/

Setting up Elastic Search properly was tedious and soul breaking

Lack of documentation and constantly going backwards and forwards from SO made me look about 10 years older.

Elastic Search returns no results if no keywords are provided

Super annoying…

Schema flexible but not

I recently made a mistake in my documents which mean I needed to resave an object as string, of course, Lucene is not schema flexible which means, despite trying to be, Elastic is not either.

So what do I do now? Re-update the index through a specially designed script (WTF why can’t I do this shit in the fucking config???) and then reapply all documents? Or delete the index and reapply all types and mappings again and reinsert all indexes again…

No easy document management

It is like providing MongoDB or MySQL without a console or shell.

No easy way to reindex

Nope.

Keeping the config file safe

Having the configuration done client side means that you must, as I said, have a php or whatever file in your app strucutre which can run the config.

This immediately poses a problem. How do you keep this at hand in a browser runable location without making specific sever rules etc? You can’t…

You have to turn your setup file into a full blown console file with the works that SHOULD BE IN ELASTIC SEARCH.

The diagnostics are terrible

I recently started getting red status all the time so I, as suggested by the user group, looked into the logs only to find:

[2013-12-22 12:44:24,257][INFO ][node                     ] [Dominic Fortune] version[0.90.8], pid[1494], build[909b037/2013-12-18T16:08:16Z]
[2013-12-22 12:44:24,257][INFO ][node                     ] [Dominic Fortune] initializing ...
[2013-12-22 12:44:24,265][INFO ][plugins                  ] [Dominic Fortune] loaded [], sites [HQ, head]
[2013-12-22 12:44:26,818][INFO ][node                     ] [Dominic Fortune] initialized
[2013-12-22 12:44:26,818][INFO ][node                     ] [Dominic Fortune] starting ...
[2013-12-22 12:44:26,891][INFO ][transport                ] [Dominic Fortune] bound_address {inet[/0:0:0:0:0:0:0:0:9300]}, publish_address {inet[/192.168.183.128:9300]}
[2013-12-22 12:44:29,919][INFO ][cluster.service          ] [Dominic Fortune] new_master [Dominic Fortune][5ddMpkRQTZa3TqQ-ljUabg][inet[/192.168.183.128:9300]], reason: zen-disco-join (elected_as_master)
[2013-12-22 12:44:29,951][INFO ][discovery                ] [Dominic Fortune] elasticsearch/5ddMpkRQTZa3TqQ-ljUabg
[2013-12-22 12:44:29,979][INFO ][http                     ] [Dominic Fortune] bound_address {inet[/0:0:0:0:0:0:0:0:9200]}, publish_address {inet[/192.168.183.128:9200]}
[2013-12-22 12:44:29,980][INFO ][node                     ] [Dominic Fortune] started
[2013-12-22 12:44:29,987][INFO ][gateway                  ] [Dominic Fortune] recovered [0] indices into cluster_state
[2013-12-22 12:45:07,323][INFO ][cluster.metadata         ] [Dominic Fortune] [main] creating index, cause [api], shards [5]/[2], mappings []
[2013-12-22 12:45:17,669][INFO ][cluster.metadata         ] [Dominic Fortune] [main] create_mapping [_default_]
[2013-12-22 12:45:17,680][INFO ][cluster.metadata         ] [Dominic Fortune] [main] create_mapping [help]
[2013-12-22 12:47:19,818][INFO ][node                     ] [Dominic Fortune] stopping ...
[2013-12-22 12:47:19,845][INFO ][node                     ] [Dominic Fortune] stopped
[2013-12-22 12:47:19,845][INFO ][node                     ] [Dominic Fortune] closing ...
[2013-12-22 12:47:19,856][INFO ][node                     ] [Dominic Fortune] closed
[2013-12-22 12:47:45,495][INFO ][node                     ] [Stryker, William] version[0.90.8], pid[1695], build[909b037/2013-12-18T16:08:16Z]
[2013-12-22 12:47:45,496][INFO ][node                     ] [Stryker, William] initializing ...
[2013-12-22 12:47:45,502][INFO ][plugins                  ] [Stryker, William] loaded [], sites [HQ, head]
[2013-12-22 12:47:48,068][INFO ][node                     ] [Stryker, William] initialized
[2013-12-22 12:47:48,068][INFO ][node                     ] [Stryker, William] starting ...
[2013-12-22 12:47:48,140][INFO ][transport                ] [Stryker, William] bound_address {inet[/0:0:0:0:0:0:0:0:9300]}, publish_address {inet[/192.168.183.128:9300]}
[2013-12-22 12:47:51,170][INFO ][cluster.service          ] [Stryker, William] new_master [Stryker, William][rMklMXasRDS4lURA0wQ7lQ][inet[/192.168.183.128:9300]], reason: zen-disco-join (elected_as_master)
[2013-12-22 12:47:51,198][INFO ][discovery                ] [Stryker, William] elasticsearch/rMklMXasRDS4lURA0wQ7lQ
[2013-12-22 12:47:51,222][INFO ][http                     ] [Stryker, William] bound_address {inet[/0:0:0:0:0:0:0:0:9200]}, publish_address {inet[/192.168.183.128:9200]}
[2013-12-22 12:47:51,223][INFO ][node                     ] [Stryker, William] started
[2013-12-22 12:47:51,242][INFO ][gateway                  ] [Stryker, William] recovered [1] indices into cluster_state

…nothing. Their diagnoistics are terribad to say the least. Currently their advice is to remove the data directory and lose ALL YOUR DATA IN THE PROCESS.

That brings me onto the next point:

No real journaling

No easy recovery if things go terribly wrong.

restarting a node always results in red status (still not found solution)

I cannot seem to restart a node via a kill command or restarting the OS without being left with red status and having to delete the data directory and lose all my data.

Elastic Search works by quorum (if you don’t know what democracy is try searching Wikipedia if you can…)

The last point was my own stupidity, remember that elastic search works via Quorum.

That is why if you set 5 shards and 2 replicas with no data you will always get red status on restart…

Auto join sharding is a really bad idea

Great in theory but what if you accidently start two nodes on the same machine because you were tired or something? OH NOES I JUST LOST MY ENTIRE CONFIG!!! When you restart that node without the other extra one on it you are left with nothing short of a disaster and you have no indication on how to fix it.

I have now had to delete all my data three times due to noob mistakes making learning Elastic Search ironically more difficult than if I had to learn sharding commands separately.

I heard once MongoDB was going to do something similar, please for the love of God don’t.

Javascript, JQuery, PHP, Web Development

PHP Upload Progress bar V2

This now has a Github home: Here!!

Updated: The new update can now handle stopping uploads as well and extra validation and more robust scripting.

Some of you will notice a long time ago I wrote a post on creating a upload progress bar in PHP. Well I have finally got round to recreating this script and I can thankfully say it is a lot better than my old script. It is, in fact, so different I am going to start anew here and describe from the beginning how I did it.

Ok so here we are, first thing you need to do is install uploadprogress extension for PHP. To do this simply run this command:

pecl install uploadprogress

If console tells you that pecl is not a valid command or that you need phpiz; just enter this into it:

apt-get install php5-pear
apt-get install php5-dev

Afterwards open up your php.ini and at the end put:

extension=uploadprogress.so

And restart your server like so (for Apache):

sudo /etc/init.d/apache2 restart

You must restart your server, reloading will not work.

Beleive it or not but you now have PHP upload progress identification installed. Now you just need to make a script that will upload a file and display its progress. Shall we move on?

The javascript

This is the backbone to the entire script. It is what allows the front to join with the back.

var upload_timer;
var timeout = 5000;
var u_ids = [];

$(document).ready(function(){

	// Add a new upload form
	add_upload();

	// When the "Add new upload" link is clicked add a new upload form
	$('#add_upload_form').click(function(){ add_upload(); return false; });

});

/**
 * Stops an upload
 *
 * @param id
 */
function stop_upload(id){

	var answer = confirm("Are you sure you wish to remove this upload?"), p_id = "#up_prog"+id;

	// Only remove the upload if it has not completed.
	if (answer && ($(p_id+" .uploadProgInner").css("width") != "100%") && ($("#container_outer"+id).length > 0)){

		// Form ids and write the cancelling message
		var message_id = "#up_message_prog"+id;
		$(message_id).html("<b style='color:red;'>Cancelling...</b>");

		// Make the upload go away peacefully showing the user it has been cancelled.
		setTimeout(function(){
			$("#container_outer"+id).remove();
			u_ids.splice(u_ids.indexOf(id), 1);
		}, "2000");
	}else{
		if(($(id+" .uploadProgInner").css("width") == "100%") || ($("#container_outer"+id).length == 0)){
			$("#general_error").show();
			$("#general_error span").html("<h2>File Upload Could Not Be Removed</h2><p>The file upload could not be removed. This is most likely due to the file having had already been completed or already removed by some other means.</p>");
		}
	}
}

/**
 * This function runs when a upload is submitted. It checks the
 * file field as best it can for a valid file. TBH there are some things
 * we just have to do AFTER the file has been uploaded to /temp
 *
 * @param id
 * @returns {Boolean}
 */
function check_upload(id){
	var file = $("#"+id).val(), end = file.length, start = end - 5;

	var ext = file.substring(start, end);

	// Is the file a divx extension?
	if(ext == ".divx"){
		$("#uploadForm-outer"+id).hide();
		$("#up_prog"+id).show();
	}else{
		$("#general_error").show();
		$("#general_error span").html("<h2>Wrong File Type</h2><p>You attempted to upload a file type that is not supported by this uploader. Pleae try again soon, we hope to have support for mkv, avi and other formats in their raw form.</p>");
		return false;
	}
}

/**
 * This adds a new upload form to the screen
 */
function add_upload(){

	var ts, count;

	// We make a ts as a cache buster for shit browsers cough-Opera-cough
	ts = Math.round(new Date().getTime() / 1000);

	// How many forms already exist?
	count = $(".uploadFormContainer").length;

	// So long as limit has not been reached (10)
	if(count < 10){

		// Generate the form
		$.get("/upload/add", {ts: ts}, function(data){

			// Add to the page
			$("#uploadForm_container-outer").append(data);

			// If this is the first form then add the updater iframe
			if(count == 0 && $('#uInfo_ifr').length == 0){
				$("#u_iframe_container").html("<a href="/upload/getInfo">/upload/getInfo</a>");
			}

			// Add the upload id to the list of IDs
			var e = $(".uploadFormContainer").last().find("input[name=UPLOAD_IDENTIFIER]").val();
			count_ids = u_ids.length;
			u_ids[count_ids] = e;
		});

	}else{

		// Show upload limit error
		$("#general_error").show();
		$("#general_error span").html("<h2>Upload Limit Reached</h2><p>This form has been limited to 10 uploads to protected internet connections. If you are sure you understand what your ISP allows you to upload you can open a new window and continue uploading there.</p>");
	}
}

/**
 * This gets the updated status of the uploads
 *
 * @param info
 */
function update_progress(info){

	// Scrolls through the IDs assigning the information.
	for(i = 0; i < info.length; i++){

		// Sometimes uploadprogress can return null for a upload
		if(info[i] != null){

			// Calculate how much has been uploaded
			var done = Math.floor(100 * parseInt(info[i].uploaded) / parseInt(info[i].total));

			// Ascertain the IDs for the elements needing change
			var id = "#up_prog"+info[i].id;
			var message_id = "#up_message_prog"+info[i].id;

			// Change the width of the progress bar to match done and set a message for the upload status
			$(id+" .uploadProgInner").css("width", done+"%");
			$(message_id+" span").html(done+"% Completed of "+info[i].file+" - Estimated Time Left: "+info[i].left+" at "+info[i].speed+"ps");
		}
	}

}

The Upload Form

Now we have the Javascript we need the upload form itself. This form will be gotten everytime add_upload() is called within the Javascript whilst the user is under the upload limit:

		global $session;

		$u_id = strval(new MongoId());
		?>

		<div class="uploadFormContainer" id="container_outer<?php echo $u_id ?>">
			<div class="uploadForm" id="uploadForm-outer<?php echo $u_id ?>">

				<h2>Please select a file to upload</h2>

				<form onsubmit='check_upload("<?php echo $u_id ?>")' action="/upload/to_server" target="u_ifr<?php echo $u_id ?>" method="post" enctype="multipart/form-data">
					<input type="hidden" name="UPLOAD_IDENTIFIER" value="<?php echo $u_id ?>" />
					<input type="file" name="<?php echo $u_id ?>" id="<?php echo $u_id ?>"/>

					<input type="submit" value="Upload" class="uploadFormSubmit"/>
				</form>

				
			</div>

			<div class="uploadBar" id="up_prog<?php echo $u_id ?>">
				<div class="uploadProgOuter">
					<div class="uploadProgInner">&nbsp;</div>
				</div>
				<div class="up_message_prog" id="up_message_prog<?php echo $u_id ?>">
					<span>Connecting to server...</span><a href="javascript:;" class="cancel_uploadForm" onclick='stop_upload("<?php echo $u_id ?>")'><b>Cancel</b></a>
				</div>
				<div id="up_err<?php echo $u_id ?>">

				</div>
			</div>
		</div>
		<?php

The Updater page

Now we have a form up and running (I will let you decipher the form :P) we just need the progress update page:

		// A nice little hack for telling IE we wont abuse iframe permissions
		header('P3P: CP="CAO PSA OUR"');

		global $session;

		?>
		<html>
			<head>
				<title>Upload Info Page</title>

			    <script type="text/javascript">

			    	// Get the u_ids from the parent window
					var u_ids = parent.u_ids, id_url = "";

			    	// Run the initial function
					refreshiframe();

				    function refreshiframe(){

				    	// Get the url formatted version of u_ids (upload IDs)
				    	if(u_ids != null && u_ids.length > 0){
				    		for(i = 0; i<u_ids.length; i++){
				    			// return an url of the current id
				    			if(i == 0){
				    				id_url = id_url + "ids[]="+u_ids[i];
				    			}else{
				    				id_url = id_url + "&ids[]="+u_ids[i];
				    			}
				    		}
				    	}else{
					    	id_url = "ids[]=0";
				    	}

				    	// if php detects the set $_GET then delay the refresh else do it now
				    	<?php if(empty($_GET['ids']) && !isset($_GET['ids'])){ ?>
				    		redirect();
				    	<?php }else{ ?>
				    		setTimeout("redirect()",5000);
				    	<?php } ?>

				    }

				    function redirect(){
					    // redirect with full url encoded u_ids
				    	parent.uInfo_ifr.location.href="/upload/getInfo?"+id_url;
				    }
			    </script>

			</head>

			<body>
				<script type="text/javascript">

					var info = [];

					<?php for($i = 0; $i<count($_GET['ids']); $i++){

						$upload_id = $_GET['ids'][$i];

						// This is our magic function. It gets our upload information
						$info = uploadprogress_get_info($upload_id);

						// We format the data to be printed into JS JSON arrays and echo it to screen
						if(!empty($info) && $info){ ?>
							info[<?php echo $i ?>] = {'id':'<?php echo $upload_id ?>', 'file':'<?php echo $info['filename'] ?>', 'uploaded':<?php echo $info['bytes_uploaded'] ?>, 'total':<?php echo $info['bytes_total'] ?>, 'left':'<?php echo gmdate("H:i:s", $info['est_sec']) ?>', 'speed':'<?php echo convert_size_human($info['speed_average']) ?>'};
						<?php }else{ ?>
							info[<?php echo $i ?>] = null;
						<?php } ?>

					<?php } ?>

					// Now lets update the upload progress
					parent.update_progress(info);

				</script>
			</body>
		</html>
		<?php

The Page

Now we need the other page this all goes into:

<div class="UIMessage error UIMessageConstrained UIUploadErrorMessage" id="general_error">
	<span></span>
</div>

<div class="upload_container">

<div id="uploadForm_container-outer"></div>

<div id="u_iframe_container" class="u_iframe_container"></div>

<a href="" id="add_upload_form">Add new upload</a>

</div>

Beleive it or not but that is it! You now know how to make a progress bar in PHP. Rmember this isn’t flash nor is it HTML 5 it will work on any browser with almost any settings so long as they don’t have JS turned off :P. All you need to do now is style it. Here is my style sheet to get you started:

.upload_container{
	width:600px;
	margin:auto;
}

.uploadFormContainer{
	padding:10px;
	border:1px solid #C9D7F1;
	margin-bottom:10px;
}

.uploadForm{
	border:1px solid #E1ECFE;
	background:none repeat scroll 0 0 #F0F6FF;
	padding:10px;
}

.uploadForm h2{
	margin-bottom:10px;
}

.uploadFormSubmit{
	float:right;
	font-weight:bold;
}

.uploadBar{
	display:none;
}

.uploadProgOuter{
	background:white;
	height:40px;
	padding:2px;
	border:1px solid #73A6FF;
}

.uploadProgInner{
	background:#73A6FF;
	width:10%;
	height:40px;
}

.up_message_prog{
	margin-top:7px;
}

.u_iframe_container{
	display:none;
}

.UIUploadErrorMessage{
	display:none;
	margin: 15px auto;
}

This style produces this result (or close anyway):

Any questions? If so just write a comment otherwise enjoy :).

PHP

PHP List of timezones (Not sorted by country)

<?php
$timezones[] = array("abbr"=>"ACDT","name"=>"Australian Central Daylight Time","offset"=>"+10:30");
$timezones[] = array("abbr"=>"ACST","name"=>"Australian Central Standard Time","offset"=>"+9:30");
$timezones[] = array("abbr"=>"ACT","name"=>"ASEAN Common Time","offset"=>"+8");
$timezones[] = array("abbr"=>"ADT","name"=>"Atlantic Daylight Time","offset"=>"-3");
$timezones[] = array("abbr"=>"AEDT","name"=>"Australian Eastern Daylight Time","offset"=>"+11");
$timezones[] = array("abbr"=>"AEST","name"=>"Australian Eastern Standard Time","offset"=>"+10");
$timezones[] = array("abbr"=>"AFT","name"=>"Afghanistan Time","offset"=>"+4:30");
$timezones[] = array("abbr"=>"AKDT","name"=>"Alaska Daylight Time","offset"=>"-8");
$timezones[] = array("abbr"=>"AKST","name"=>"Alaska Standard Time","offset"=>"-9");
$timezones[] = array("abbr"=>"AMST","name"=>"Armenia Summer Time","offset"=>"+5");
$timezones[] = array("abbr"=>"AMT","name"=>"Armenia Time","offset"=>"+4");
$timezones[] = array("abbr"=>"ART","name"=>"Argentina Time","offset"=>"-3");
$timezones[] = array("abbr"=>"AST","name"=>"Arab Standard Time (Kuwait, Riyadh)","offset"=>"+3");
$timezones[] = array("abbr"=>"AST","name"=>"Arabian Standard Time (Abu Dhabi, Muscat)","offset"=>"+4");
$timezones[] = array("abbr"=>"AST","name"=>"Arabic Standard Time (Baghdad)","offset"=>"+3");
$timezones[] = array("abbr"=>"AST","name"=>"Atlantic Standard Time","offset"=>"-4");
$timezones[] = array("abbr"=>"AWDT","name"=>"Australian Western Daylight Time","offset"=>"+9");
$timezones[] = array("abbr"=>"AWST","name"=>"Australian Western Standard Time","offset"=>"+8");
$timezones[] = array("abbr"=>"AZOST","name"=>"Azores Standard Time","offset"=>"-1");
$timezones[] = array("abbr"=>"AZT","name"=>"Azerbaijan Time","offset"=>"+4");
$timezones[] = array("abbr"=>"BDT","name"=>"Brunei Time","offset"=>"+8");
$timezones[] = array("abbr"=>"BIOT","name"=>"British Indian Ocean Time","offset"=>"+6");
$timezones[] = array("abbr"=>"BIT","name"=>"Baker Island Time","offset"=>"-12");
$timezones[] = array("abbr"=>"BOT","name"=>"Bolivia Time","offset"=>"-4");
$timezones[] = array("abbr"=>"BRT","name"=>"Brasilia Time","offset"=>"-3");
$timezones[] = array("abbr"=>"BST","name"=>"Bangladesh Standard Time","offset"=>"+6");
$timezones[] = array("abbr"=>"BTT","name"=>"Bhutan Time","offset"=>"+6");
$timezones[] = array("abbr"=>"CAT","name"=>"Central Africa Time","offset"=>"+2");
$timezones[] = array("abbr"=>"CCT","name"=>"Cocos Islands Time","offset"=>"+6:30");
$timezones[] = array("abbr"=>"CDT","name"=>"Central Daylight Time (North America)","offset"=>"-5");
$timezones[] = array("abbr"=>"CEDT","name"=>"Central European Daylight Time","offset"=>"+2");
$timezones[] = array("abbr"=>"CET","name"=>"Central European Time","offset"=>"+1");
$timezones[] = array("abbr"=>"CHAST","name"=>"Chatham Standard Time","offset"=>"+12:45");
$timezones[] = array("abbr"=>"CIST","name"=>"Clipperton Island Standard Time","offset"=>"-8");
$timezones[] = array("abbr"=>"CKT","name"=>"Cook Island Time","offset"=>"-10");
$timezones[] = array("abbr"=>"CLST","name"=>"Chile Summer Time","offset"=>"-3");
$timezones[] = array("abbr"=>"CLT","name"=>"Chile Standard Time","offset"=>"-4");
$timezones[] = array("abbr"=>"COST","name"=>"Colombia Summer Time","offset"=>"-4");
$timezones[] = array("abbr"=>"COT","name"=>"Colombia Time","offset"=>"-5");
$timezones[] = array("abbr"=>"CST","name"=>"Central Standard Time (North America)","offset"=>"-6");
$timezones[] = array("abbr"=>"CST","name"=>"China Standard Time","offset"=>"+8");
$timezones[] = array("abbr"=>"CVT","name"=>"Cape Verde Time","offset"=>"-1");
$timezones[] = array("abbr"=>"CXT","name"=>"Christmas Island Time","offset"=>"+7");
$timezones[] = array("abbr"=>"ChST","name"=>"Chamorro Standard Time","offset"=>"+10");
$timezones[] = array("abbr"=>"DFT","name"=>"AIX Central European Time","offset"=>"+1");
$timezones[] = array("abbr"=>"EAST","name"=>"Easter Island Standard Time","offset"=>"-6");
$timezones[] = array("abbr"=>"EAT","name"=>"East Africa Time","offset"=>"+3");
$timezones[] = array("abbr"=>"ECT","name"=>"Eastern Caribbean Time","offset"=>"-4");
$timezones[] = array("abbr"=>"ECT","name"=>"Ecuador Time","offset"=>"-5");
$timezones[] = array("abbr"=>"EDT","name"=>"Eastern Daylight Time (North America)","offset"=>"-4");
$timezones[] = array("abbr"=>"EEDT","name"=>"Eastern European Daylight Time","offset"=>"+3");
$timezones[] = array("abbr"=>"EEST","name"=>"Eastern European Summer Time","offset"=>"+3");
$timezones[] = array("abbr"=>"EET","name"=>"Eastern European Time","offset"=>"+2");
$timezones[] = array("abbr"=>"EST","name"=>"Eastern Standard Time (North America)","offset"=>"-5");
$timezones[] = array("abbr"=>"FJT","name"=>"Fiji Time","offset"=>"+12");
$timezones[] = array("abbr"=>"FKST","name"=>"Falkland Islands Standard Time","offset"=>"-4");
$timezones[] = array("abbr"=>"GALT","name"=>"Galapagos Time","offset"=>"-6");
$timezones[] = array("abbr"=>"GET","name"=>"Georgia Standard Time","offset"=>"+4");
$timezones[] = array("abbr"=>"GFT","name"=>"French Guiana Time","offset"=>"-3");
$timezones[] = array("abbr"=>"GILT","name"=>"Gilbert Island Time","offset"=>"+12");
$timezones[] = array("abbr"=>"GIT","name"=>"Gambier Island Time","offset"=>"-9");
$timezones[] = array("abbr"=>"GMT","name"=>"Greenwich Mean Time","offset"=>"0");
$timezones[] = array("abbr"=>"GST","name"=>"South Georgia and the South Sandwich Islands","offset"=>"-2");
$timezones[] = array("abbr"=>"GYT","name"=>"Guyana Time","offset"=>"-4");
$timezones[] = array("abbr"=>"HADT","name"=>"Hawaii-Aleutian Daylight Time","offset"=>"-9");
$timezones[] = array("abbr"=>"HAST","name"=>"Hawaii-Aleutian Standard Time","offset"=>"-10");
$timezones[] = array("abbr"=>"HKT","name"=>"Hong Kong Time","offset"=>"+8");
$timezones[] = array("abbr"=>"HMT","name"=>"Heard and McDonald Islands Time","offset"=>"+5");
$timezones[] = array("abbr"=>"HST","name"=>"Hawaii Standard Time","offset"=>"-10");
$timezones[] = array("abbr"=>"IRKT","name"=>"Irkutsk Time","offset"=>"+8");
$timezones[] = array("abbr"=>"IRST","name"=>"Iran Standard Time","offset"=>"+3:30");
$timezones[] = array("abbr"=>"IST","name"=>"Indian Standard Time","offset"=>"+5:30");
$timezones[] = array("abbr"=>"IST","name"=>"Irish Summer Time","offset"=>"+1");
$timezones[] = array("abbr"=>"IST","name"=>"Israel Standard Time","offset"=>"+2");
$timezones[] = array("abbr"=>"JST","name"=>"Japan Standard Time","offset"=>"+9");
$timezones[] = array("abbr"=>"KRAT","name"=>"Krasnoyarsk Time","offset"=>"+7");
$timezones[] = array("abbr"=>"KST","name"=>"Korea Standard Time","offset"=>"+9");
$timezones[] = array("abbr"=>"LHST","name"=>"Lord Howe Standard Time","offset"=>"+10:30");
$timezones[] = array("abbr"=>"LINT","name"=>"Line Islands Time","offset"=>"+14");
$timezones[] = array("abbr"=>"MAGT","name"=>"Magadan Time","offset"=>"+11");
$timezones[] = array("abbr"=>"MDT","name"=>"Mountain Daylight Time (North America)","offset"=>"-6");
$timezones[] = array("abbr"=>"MIT","name"=>"Marquesas Islands Time","offset"=>"-9:30");
$timezones[] = array("abbr"=>"MSD","name"=>"Moscow Summer Time","offset"=>"+4");
$timezones[] = array("abbr"=>"MSK","name"=>"Moscow Standard Time","offset"=>"+3");
$timezones[] = array("abbr"=>"MST","name"=>"Malaysian Standard Time","offset"=>"+8");
$timezones[] = array("abbr"=>"MST","name"=>"Mountain Standard Time (North America)","offset"=>"-7");
$timezones[] = array("abbr"=>"MST","name"=>"Myanmar Standard Time","offset"=>"+6:30");
$timezones[] = array("abbr"=>"MUT","name"=>"Mauritius Time","offset"=>"+4");
$timezones[] = array("abbr"=>"NDT","name"=>"Newfoundland Daylight Time","offset"=>"-2:30");
$timezones[] = array("abbr"=>"NFT","name"=>"Norfolk Time","offset"=>"+11:30");
$timezones[] = array("abbr"=>"NPT","name"=>"Nepal Time","offset"=>"+5:45");
$timezones[] = array("abbr"=>"NST","name"=>"Newfoundland Standard Time","offset"=>"-3:30");
$timezones[] = array("abbr"=>"NT","name"=>"Newfoundland Time","offset"=>"-3:30");
$timezones[] = array("abbr"=>"OMST","name"=>"Omsk Time","offset"=>"+6");
$timezones[] = array("abbr"=>"PDT","name"=>"Pacific Daylight Time (North America)","offset"=>"-7");
$timezones[] = array("abbr"=>"PETT","name"=>"Kamchatka Time","offset"=>"+12");
$timezones[] = array("abbr"=>"PHOT","name"=>"Phoenix Island Time","offset"=>"+13");
$timezones[] = array("abbr"=>"PKT","name"=>"Pakistan Standard Time","offset"=>"+5");
$timezones[] = array("abbr"=>"PST","name"=>"Pacific Standard Time (North America)","offset"=>"-8");
$timezones[] = array("abbr"=>"PST","name"=>"Philippine Standard Time","offset"=>"+8");
$timezones[] = array("abbr"=>"RET","name"=>"Réunion Time","offset"=>"+4");
$timezones[] = array("abbr"=>"SAMT","name"=>"Samara Time","offset"=>"+4");
$timezones[] = array("abbr"=>"SAST","name"=>"South African Standard Time","offset"=>"+2");
$timezones[] = array("abbr"=>"SBT","name"=>"Solomon Islands Time","offset"=>"+11");
$timezones[] = array("abbr"=>"SCT","name"=>"Seychelles Time","offset"=>"+4");
$timezones[] = array("abbr"=>"SLT","name"=>"Sri Lanka Time","offset"=>"+5:30");
$timezones[] = array("abbr"=>"SST","name"=>"Samoa Standard Time","offset"=>"-11");
$timezones[] = array("abbr"=>"SST","name"=>"Singapore Standard Time","offset"=>"+8");
$timezones[] = array("abbr"=>"TAHT","name"=>"Tahiti Time","offset"=>"-10");
$timezones[] = array("abbr"=>"THA","name"=>"Thailand Standard Time","offset"=>"+7");
$timezones[] = array("abbr"=>"UTC","name"=>"Coordinated Universal Time","offset"=>"0");
$timezones[] = array("abbr"=>"UYST","name"=>"Uruguay Summer Time","offset"=>"-2");
$timezones[] = array("abbr"=>"UYT","name"=>"Uruguay Standard Time","offset"=>"-3");
$timezones[] = array("abbr"=>"VET","name"=>"Venezuelan Standard Time","offset"=>"-4:30");
$timezones[] = array("abbr"=>"VLAT","name"=>"Vladivostok Time","offset"=>"+10");
$timezones[] = array("abbr"=>"WAT","name"=>"West Africa Time","offset"=>"+1");
$timezones[] = array("abbr"=>"WEDT","name"=>"Western European Daylight Time","offset"=>"+1");
$timezones[] = array("abbr"=>"WET","name"=>"Western European Time","offset"=>"0");
$timezones[] = array("abbr"=>"YAKT","name"=>"Yakutsk Time","offset"=>"+9");
$timezones[] = array("abbr"=>"YEKT","name"=>"Yekaterinburg Time","offset"=>"+5");