On June 18, Yandex
announced public access to the Yandex Direct API . We, as an advertising agency, received this access a bit earlier and now we use it to manage the rates in advertising campaigns. I would like to share our experience.
Our initial impressions of the API were rather contradictory, all the same it is noticeable that this is the first version. In some places there are inconsistencies in the naming of API elements, often excessive, in our opinion, the system of types of method parameters.
')
Our goal was to provide project developers with a relatively simple mechanism for describing rules for automatic bidding, while the developer should easily describe trivial situations and be able to implement an arbitrary algorithm if required by advertising or promotion of Internet projects.
Using a clean API in this regard, we found it unpromising :) and decided to write a more convenient wrapper over it.
We immediately faced one unpleasant moment. The API uses the SOAP protocol, the server uses SOAP :: Lite. SOAP :: Lite has some specific features when forming the representation of arrays of objects returned by SOAP methods, as a result of which using the standard PHP-module SOAP Client on the client it becomes impossible to use the classmap option.
This option allows you to associate SOAP data types with classes of objects that represent these types, and without it, it’s generally bad. With other implementations of a SOAP server, for example, in Java, this problem does not arise, has been tested on Google services and various others. Frankly, our knowledge of SOAP is not enough to offer a reasonable way out of this situation, the problem is likely on the side of PHP, and not SOAP :: Lite. Yandex, in turn, recommends using NuSOAP.
In general, the protocol seems to be standard, but there is no happiness :(
By the way, for normal operation, say, with the Google Adwords API, the query formed by the standard PHP Soap Client has to be patched mercilessly; examples can be viewed in the source code of the PHP client for this service.
We were afraid to use NuSOAP and decided to do without a classmap, working with simple stdClass objects that the API returns, adding behavior to them with the help of wrappers. In addition, the wrappers perform the translation of property names from our standard (with underscores) to CamelCase - it's good when everything is the same.
The vast majority of API methods return lists of objects. In some cases it is convenient not only to use them as containers, but also to attach some kind of behavior to them, so we made the base class of the collections Service.Yandex.Direct.Collection, which:
- stores the returned data as ordinary stdclass structures
- when accessing by index, it creates a wrapper over the corresponding elements of the collection, so data is stored inside, and wrappers are created as needed
- allows you to select individual elements not only by the ordinal index, but also by the value of the identifier of the stored entity;
- allows to generate child collections that contain only elements that satisfy a certain condition;
- Allows you to call API methods that operate on collection data (if supported).
Actually, access to the API was placed in the Service.Yandex.Direct.APIMapper class, which connects to the service, dispatches SOAP calls using __call, and tries to be polite, making pauses between requests if necessary (at least for testing time there were restrictions on the frequency of calls ).
Thus, a developer can call API methods in standard notation of our library, with call logging, keeping time between them, etc.
Based on this, we add the behavior to the entities and collections. Using our own collections is very useful to us, because it allows us, for example, to write like this:
Service_Yandex_Direct::api()-> campaigns_for($login)-> by_id($campaign_id)-> all_banners()-> by_id($banner_id)-> all_phrases();
Currently, for most of the samples in the API, it is not possible to apply filtering rules (some methods allow, but not all). Since we needed sampling under various conditions, we had to implement it on the client again in the base class of the collection.
It looks like this:
$phrases-> select()-> where(array('phrase ~' => array('{^ }', '{^ }')));
In this case, from the set of phrases that was returned by the web service, we select phrases starting with “advertising photography” and “advertising photography”.
A where condition is an array, the keys of which contain the name of the field on which the condition is imposed, and the operation that is used for matching, and the values ​​represent a sample or a list of samples with which matching is made. The result of the execution is a collection of the same class as the original one, but containing filtered elements.
While we have enough of such operations:
- ~ - matching with a regular expression;
- = - equality test;
- in - check for entry into a set of values.
- ! in - check for the absence of a set of values.
Well, the behavior of the collections and entities also help:
Service_Yandex_Direct::api()-> campaigns_for($client)-> all_banner()-> where(array('id in', $ids))-> stop();
Now how we adapted all this for project developers.
For each project, an advertising campaign management script is created. It is a PHP script, such as the following:
<?php $this-> stay_special(5.0, $campaigns->by_id(2059530)-> all_banners()-> where(array('BannerID in' => array(6989002, 6989179, 6989186, 6989202, 6989206, 6989209, 6989212))), 0.01)-> try_special(2.0, $campaigns->by_id(2059530)-> all_banners()-> where(array('BannerID !in' => array(6989002, 6989179, 6989186, 6989202, 6989206, 6989209, 6989212))), 0.01); ?>
Scripts are launched by the command line application, implemented as a module Service.Yandex.Direct.Manager, the application, in turn, is launched by cron.
The variable $ campaigns is automatically created by the task class and contains a list of campaigns for the current client, which is determined by the name of the script file.
In each call we define a list of ads for which you need to change prices. This is done by sequentially sampling the “campaign → ads → phrases”.
In the first case:
$campaigns
- all client campaigns;$campaigns->by_id(2059530)
- campaign with id = 2059530;$campaigns->by_id(2059530)->all_banners()
- all campaign announcements with id = 2059530;$campaigns->by_id(2059530)->all_banners()->where(array('BannerID in' => array(...)))
- all campaign announcements with id = 2059530, with the identifiers listed in
Thus, in words, the above expression can be described as follows: all advertisements of the company 2059530 of the client studylab, falling into the specified id list.
In fact, we are interested in prices, and prices are tied to phrases. However, the application itself is able to complete the missing links in the chain. In our case, the full expression would look like this:
$campaigns-> by_id(2059530)-> all_banners()-> where(array('BannerId in' => array(...)))-> all_phrases();
In principle, nothing prevents you from writing code in the script file that performs certain actions on prices and then calls the standard update_prices () method. For example, you could write this:
$phrases = $campaigns->by_id(2059530)->all_banners()->all_phrases(); $prices = $phrases->prices; foreach ($phrases as $phrase) $prices->by_id($phrase->id)->price = $phrase->premium_min < $limit ? (($phrase->premium_min + $delta < $phrase->premium_max) ? $phrase->premium_min + $delta : $phrase->premium_min + 0.01) : $limit; $this->api->update_prices($prices);
The above code passes through all the phrases, checks whether the current rate provides a display in special allocations, and adjusts it accordingly.
If the project imposes some special requirements on campaign management, this should be done, however, in most projects, price management tasks are usually similar and very simple, so these standard actions can be distinguished into ready-made methods that implement this or that rate change strategy.
Three strategies are enough for us now, but probably we will need more:
stay_special
- hold ad in special placements (but not necessarily in the first place)stay_visible
- hold ad in shows;try_special
- to keep an ad in shows, trying to get into special placements whenever possible.- Each of these strategies is parameterized by the maximum value of the bet and the delta by which it can be increased. The default delta is 0.01.
Thus, instead of the above code, it is enough to write (for example, for all the phrases of a given client)
$this->stay_special($campaigns->by_id(2059530));
The programmer tests the script, after which the script is added to the application that manages the campaigns. So far we have enough of one process, but in general it is possible to run several instances of the campaign manager with separate queues for different clients.
What did we get in the end? Writing an object wrapper over the SOAP API allowed us to minimize the amount of code to solve routine tasks and at the same time make this code sufficiently readable so that, if necessary, even a non-programmer could understand by what rules the rates are formed. In this case, we leave for the programmer the possibility of implementing an arbitrary algorithm for manipulating the rates.
The module code is
available on github ,
our internal PHP framework is used .