jakubův notes – programování a vejšplechty

Píšu o:

Cesta za ideálním šablonovacím jazykem

Začalo to problémem, pokračovalo různými inspiracemi a skončilo řešením.

Potíž s odkazovaným problémem v běžných šablonovacích systémech (Smarty-like) je ten, že jazyk, jakým jsou šablony v Smarty-like systémech psány, je imperativní. U šablon se všeobecně podle mě hodí více deklarativní způsob vyjadřování – šablonovači řeknu pouze to, co má udělat a jak to konkrétně udělá, mě nezajímá. Příklad: říct šablonovači „chci vypsat seznam položek oddělených čárkami“ je jednodušší než psát „pro každou položku v seznamu: pokud je to první položka, vytiskni pouze tuhle položku; jinak vytiskni čárku a potom teprve položku“.

Další věcí, která by se dala označit jako „jQuery idiom“ (terminus technicus jako noha), je, že nezáleží, jestli je operace aplikována na jednu, nebo více položek. Je jedno, jestli mám jen jednu položku, nebo jich je milion – já chci, aby každá položka byla obklopena tagem <b>.

A třetí podstatnou věcí je kontext. Kontext obsahuje vstupní data a zprostředkovává je šabloně.

Syntaxe šablon staví na tom, že šablony jsou bloky textu s dírami, které chtějí něčím vyplnit. Aby bylo poznat, kde díra začíná, je použit znak dolaru. Po něm následuje buď určení podkontextu, nebo filtr. Volitelně se na výsledek dají aplikovat další filtry oddělené dvojtečkou. Znak, který by oznamoval ukončení díry je podle mě nadbytečný a nenapomáhá čitelnosti.

Nejdříve případ, kdy následuje určení podkontextu. Podkontext se dá určit selektorem. Řekněme, že kontext je name = "world" a šablona:

Hello, $name!

Výsledkem bude Hello, world!. Selektor odpovídá regulárnímu výrazu [a-zA-Z0-9_]+. Vícenásobný selektor je více takových selektorů oddělených tečkami:

Hello, $person.name.first!

V kontextu person = (name = (first = "Fred")) vytiskne Hello, Fred!.

Krom selektoru může podkontext také určit pole, nebo řetězec:

$"Hello", $(1, 2, 3, 4, 5)!

Řetězec je sekvence znaků ohraničených uvozovkami. Pole je kolekce položek oddělených čárkami. Výstupem předchozího kódu v jakémkoli kontextu bude: Hello, 12345! Kontext řetězce vrací na výstup samotný řetězec. Pole vrací na výstup postupně výstup každého prvku.

To pravě ořechové začíná, když se začnou používat filtry. Nejjednodušším filtrem je aplikace anonymní šablony:

$(1, 2, 3, 4, 5):{$$, }

Jelikož anonymní šablona je filtr, je od selektoru oddělena dvojtečkou. Text šablony je ohraničen složenými závorkami. Protože kontextem pro anonymní šablonu je pole, je šablona aplikována na každou položku a výsledkem je pole nových položek. $$ je aktuální kontext. Výsledkem šablony bude: 1, 2, 3, 4, 5, . Krom $$ je dalším speciálním kontextem $# který obsahuje klíč pole:

$(foo = "bar", "bar" = "baz"):{$# = $$, }

Výsledek: foo = bar, bar = baz, .

Filtrem může být také pojmenovaná šablona:

$"foo":#justprint{$$}

Výhoda pojmenované šablony je v tom, že ji nadále můžeme používat pouze pomocí jejího jména. Pokud tedy předchozí šablonu upravíme na:

$"foo":#justprint{$$}
$"bar:justprint

Výstupem bude:

foo
bar

Je také možno definovat pojmenovanou šablonu mimo výraz začínající dolarem:

#justprint{$$}
$"foo":justprint
$"bar":justprint

Výstup bude stejný jako předtím. Pojmenovaná šablona definovaná mimo výraz s dolarem nic nevytiskne, pouze se pak bude dát použít v následujících výrazech.

Posledním, vlastně už známým druhem filtru, je aplikace pojmenované funkce. V případech výše to vždycky byly šablony, které tyto filtry vytvářely, ale taková pojmenovaná funkce může být externě definovaná, někde v PHP kódu (takové funkce jsou samozřejmě mnohem mocnější). Krom toho mohou přijímat další parametr, např.:

$"foo":transform(upper):prepend{->}

Parametrem je buď pole, nebo šablona (rozdíl mezi zavoláním funkce s šablonou a aplikací pojmenované šablony je v tom, že u pojmenované šablony je na začátku mřížka). V případě šablony se jedná vlastně o jednoprvkové pole – prepend({->}).

A co když není vytvořen žádný nový kontext a za dolarem následuje už rovnou filtr? Pak se bere aktuální kontext (pozor, kdyby za justprint na druhém řádku nebyly kulaté závorky, bylo by to bráno jako selektor, nikoli jako aplikace filtru):

#justprint{$$}
$"foo":{$justprint()}

Je to samé jako:

#justprint{$$}
$"foo":{$$:justprint}

A jak by vypadal problém ze začátku v tomto šablonovacím systému?

<ul>
    $seznam:{<li><a href="/zobrazit?id=$id">$(
        "vymazat" = $vymazat,
        "upravit" = $upravit
    ):allTrue:{<a href="/$#?id=$id">$#</a>}:pss(" (", ", ", ")")</li>}
</ul>

Filtr alltrue vybere všechny pravdivé hodnoty. pss přijímá tři parametry – prefix, separator, suffix – položky pole spojí pomocí separatoru, před ně přidá prefix a za ně suffix, jednalo by se o funkci definovanou někde mimo šablony.

Jak by mohlo vypadat vypsání tabulky:

<table>
    <tr>
        <th>Jméno</th>
        <th>Heslo</th>
    </tr>

    $uzivatele:{
    <tr class="$#:class("odd", "even")">
        <td>$jmeno</td>
        <td>$heslo</td>
    </tr>
    }
</table>

Anebo hieararchické menu:

$($menu):#menu{
<ul>
    <li>
        <a href="$url">$nazev</a>
        $($podmenu):menu
    </li>
</ul>
}

vydáno 4. 2. 2010, 11:36:06

žádný komentář

Zařazeno mezi:

pacc – parser generátor pro PHP

Někdo znovuobjevuje kolo v podobě frameworků pro PHP. Já na to jdu jinak, já znovuobjevuji parser generátory.

Když jsem narazil na CoffeeScript, popadl mě (opět) nápad začít pracovat na vlastním programovacím jazyce. Takovéhle sklony mám již delší dobu, ale naštěstí se mi to vždycky podaří utlumit a začít pro změnu dělat něco užitečného. Už jsem přešel od svého ideálního dynamického imperativního přes funkcionální dynamicky typovaný až po imperativní staticky typovaný jazyk.

U CoffeeScriptu mě zaujalo hlavně to, že se zkompiluje do JavaScriptu. Takových projektů, jak na JavaScript naroubovat jiný jazyk, je tu více. Dokonce je tu i interpret YARV bytecodu. Jelikož mojí platformou volby je PHP. A PHP není zrovna jeden z jazyků, které by se mi líbily. Je to paradox – dva jazyky se mi opravdu moc nelíbí (PHP a C++) a oba jsou na první příčce seznamu, když chci něco napsat.

Inspirován CoffeeScriptem jsem se rozhodl, že jazyk (až a jestli vůbec někdy bude existovat) bude kompilován do PHP.

Jelikož psát parser (zvlášť u těch složitějších) ručně je zábava na dlouhé zimní večery, rozhodl jsem se, že je potřeba parser generátor. Nebyl bych to PHPčkař, kdybych využil třebas balíček z PEARu, nebo rovnou ANTLR PHP target, a tak jsem napsal pacc.

Je to čistě parser generátor (lexer si musíte dopsat sami) a je ve stádiu „proof of concept“ (bohužel jako většina toho, co napíšu). pacc přijímá na vstupu soubor formátu, který se jistě v budoucnu ještě mnohokrát změní.

Na příklady se podívejte do podadresáře examples/ v repozitáři. Já si tu jeden vezmu a rozeberu ho podrobněji. Bude to kalkulátor (calculator.y):

grammar Calculator

Na začátku souboru je název gramatiky. (Teď uvažuji nad tím, jestli zrovna grammar je to nejvhodnější slovo…) pacc vygeneruje tedy třídu s názvem Calculator. Za deklarací grammar může, ale nemusí být středník.

option (
    eol = "\n";
    indentation = "    ";
    parse = "doParse";
    algorithm = "LR";
)

Složená deklarace option. eol nastavuje, jaký bude znak konce řádku ve vygenerovaném souboru (ve výchozím stavu je to \n), indentation zase, čím se bude odsazovat (defaultně čtyři mezery). parse udává, jak se bude jmenovat metoda, která provede vlastní parsování (výchozí doParse). algorithm je asi nejzajímavější – nastavuje totiž, jaký výstupní typ algoritmu chcete. Zatím jsou implementovány dva – RD (recursive descent) a LR (LR parser, konkrétně canonical LR(1)). Recursive descent je nejjednodušší typ parseru a většinou se píše ručně (třebas parser použitý k naparsování souboru s gramatikou je právě ručně napsaný recursive descent). S LR je to složitější, protože tam jsou potřeba tabulky a ty je nejjednodušší právě vygenerovat.

Stejně jako s grammar deklarací, za option může a nemusí být středník (option ( ... );). Ve složeném optionu středník odděluje jednotlivá nastavení, takže za posledním být může, avšak nemusí.

Krom složeného option je tu i jednoduché. Následující kód je ekvivalentní předchozímu:

option eol = "\n"
option indentation = "    "
option parse = "doParse"
option algorithm = "LR"

Krom řetězce (přičemž řetězec je sled znaků ohraničený uvozovkami, apostrofem, nebo obráceným apostrofem) lze jako option též nastavit PHP kód. Ale pro toto je jednodušší využít syntaktického cukru:

@inner {
    const NUMBER = 1;

    private $expression;
    private $token;

    public function calculate($expression)
    {
        $this->expression = $expression;
        $this->_nextToken();
        return $this->doParse();
    }
}

@footer {
    $calculator = new Calculator;
    echo $calculator->calculate(file_get_contents('php://stdin')) . "\n";
}

PHP kód je vždy ohraničen složenými závorkami. Zavináč, nějaký název a PHP kód nastavuje danou možnost na PHP kód. Kód inner se vloží do těla vygenerované třídy, typicky obsahuje nastavení instančních a třídních proměnných a veřejných metod pro manipulaci s parserem; footer bude umístěn na konec souboru a jeho tu za účelem inicializace, která nelze provést ve třídě (v příkladě s kalkulátorem přečteme standardní vstup a necháme ho vyhodnotit). Ještě je tu header, kterýžto není sice v příkladu uveden, ale v případě, že by byl, tak se vloží nad třídu, může obsahovat např. doc-komentář.

@currentToken {
    return $this->token;
}

@currentTokenType {
    if (preg_match('~^[0-9]+$~', $this->token)) { return self::NUMBER; }
    return NULL;
}

@currentTokenLexeme {
    return $this->token;
}

@nextToken {
    if (!preg_match('~^([0-9]+|\(|\)|\+|-|\*|/)~', $this->expression, $m)) {
        $this->expression = NULL;
        $this->token = NULL;
        return;
    }

    $this->token = $m[1];
    $this->expression = substr($this->expression, strlen($m[1]));
}

Čtveřice kódů, které slouží pro komunikaci s lexerem. U kalkulátoru žádný speciální lexer není, a tak tyto kódy jsou vlastně lexerem. Každý z těchto kódů bude ve výsledku metodou ve třídě, akorát s podtržítkem na začátku (currentToken bude _currentToken, currentTokenType bude _currentTokenType atd.).

currentToken by měl vracet něco (je jedno co), co reprezentuje aktuální token. V tomto případě je to řetězec. currentTokenType typ aktuálního tokenu a currentTokenLexeme obsah, řetězcovou hodnotu aktuálního tokenu (jak to má konkrétně s těmito metodami být povím u vysvětlování pravidel). Po zavolání nextToken by všechny předchozí metody měly vracet další token / informace o dalším tokenu v streamu.

Nyní již k samotným pravidlům:

expression
    : /* nothing */ { $$ = 0; }
    | component { $$ = $1; }
    | expression '+' component { $$ = $1 + $3; }
    | expression '-' component { $$ = $1 - $3; }
    ;

component
    : factor { $$ = $1; }
    | component '*' factor { $$ = $1 * $3; }
    | component '/' factor { $$ = $1 / $3; }
    ;

factor
    : NUMBER { $$ = intval($1); }
    | '(' expression ')' { $$ = $2; }
    ;

Bezkontextové gramatiky se popisují řadou produkcí s jedním neterminálem na levé straně a žádným, či více neterminály a/nebo terminály na straně pravé. Produkce navíc může mít na levé i pravé straně stejný neterminál, což znamená, že je produkce rekurzivní. Jedno pravidlo se skládá z levé strany produkcí (tedy neterminálu), poté dvojtečky, jedné nebo více pravých stran produkcí oddělených svislítkem (|) a je ukončené středníkem. U každé pravé stranu produkce může být navíc přidružen speciální PHP kód.

pacc používá Bison(-like) notaci pro zápis pravidel, což je právě ta popsaná výše. Neterminály jsou všechny řetězce vyhovující regulárnímu výrazu [a-z][a-z0-9_], terminály jsou ohraničené řetězce jako v případě nastavování optionů, nebo sekvence znaků vyhovující [A-Z][A-Z0-9_]. Pokud je u neterminálu použit ohraničený řetězec, porovnává se s hodnotou vrácenou z metody currentTokenLexeme, jestliže se jedná o druhý případ, porovnává se s hodnotou vrácenou z currentTokenType. Pokud currentToken, currentTokenLexeme i currentTokenType vrací NULL, znamená to konec streamu.

K samotným pravidlům. Začněme od factor. factor je (dvojtečku můžeme číst jako „je“) buď terminál NUMBER (porovnává se s currentTokenType), nebo (svislítko můžeme číst jako „nebo“) výraz ohraničený kulatými závorkami.

Co je toto?

$$ = intval($1);

currentToken kalkulátoru vrací vrací číslo vypreparované z řetězce určeného k vyhodnocení. Funkce intval() převede řetězec na číslo. Proměnná s názvem 1 je speciální proměnná, ve které je uložen první symbol z pravé strany produkce; analogicky 2 je druhý, 3 je třetí atd. Dolarová proměnná je výsledek produkce – tedy neterminál na levé straně. A tak výraz výše znamená, že výsledkem produkce je číselná hodnota prvního symbolu. První symbol je terminál NUMBER, který je reprezentován číselným tokenem, takže výsledkem produkce je číselná hodnota tohoto tokenu. V případě druhé větve pravidla factor, je výsledkem hodnota druhého symbolu, tedy expression.

Stejně je to s dalšími pravidly. PHP kód u pravé strany produkce nemusí být přítomen. V tomto případě je výsledkem produkce hodnota prvního symbolu.

Kalkulátor si můžete vyzkoušet. Stáhnětě si zdrojové kódy z repozitáře, přejděte do adresáře s nimi a spusťte:

$ ./bin/pacc -i ./examples/calculator.y -fo ./calculator.php
$ echo "22*2-2" | php -f ./calculator.php
42

Praktičtější z příkladů je parser JSONu, který se může hodit v případě, že na serveru není nainstalován PHP json modul.

V budoucích verzích bych chtěl vychytat co nejvíce mušek (nepředpokládám, že by jich tam bylo zrovna málo) a nakonec ručně psaný parser pro tento formát souboru úplně nahradit parserem generovaným paccem. Taky se mi zrovna nelíbí metody currentToken apod., takže tohle může doznat dramatických změn. Psát lexery mě už taky nebaví, takže podpora jeho generování může do paccu přibýt. Dalším omezením je, že první pravidlo v souboru se nesmí jmenovat grammar, nebo option – parser by pak nepoznal, že se jedná o pravidlo a myslel si, že je to nějaká direktiva, což by skončilo parser errorem. Alternativou k tomuto omezení by bylo buď všechny deklarace něčím prefixovat, nebo pravidla do něčeho obalit. Jedno horší jak druhé, takže tu tohle omezení zůstane. Vývoj bude pravděpodobně nadále probíhat na LR parseru a RD algoritmus bude nakonec odstraněn.

vydáno 13. 1. 2010, 23:21:08

2 komentáře

Zařazeno mezi:

PHP mě nepřestává překvapovat

$true = TRUE;
$false = FALSE;

printf("%s\n%s\n%s\n%s\n",
    $true++  === TRUE  ? 'TRUE++  === TRUE'  : 'TRUE++  !== TRUE',
    $true--  === TRUE  ? 'TRUE--  === TRUE'  : 'TRUE--  !== TRUE',
    $false++ === FALSE ? 'FALSE++ === FALSE' : 'FALSE++ !== FALSE',
    $false-- === FALSE ? 'FALSE-- === FALSE' : 'FALSE-- !== FALSE'
);

Hádejte, co následující kód vytiskne?

vydáno 21. 12. 2009, 18:48:34

6 komentářů (6 nových)

Zařazeno mezi:

B+Tree v PHP

Když jsem konečně rozchodil metatable, byl to prakticky tantrický okamžik. Och, ono to vážně funguje! A ukládá! A vybírá! Jako když malé děcko dostane lízátko. Navíc, já jsem si to lízátko sám i udělal.

Ale neusnul jsem takříkajíc na vavřínech a šel jsem ještě dál. Když si vezmete snad jakoukoli databázi, tak najdete B+Tree, další B+Tree a ještě více B+Tree. Inu, proč se neinspirovat a neudělat taky takový B+Tree? Samozřejmě zcela v PHP.

A tak vznikla třída btree, která umí spravovat B+Tree uložený v souboru. API je opět minimalistické a bude vám stačit osvojit si jen pár metod – v případě btree si vystačíte s polovinou metod než u metatable (celkem tedy se třemi). Okolo metatable jsem udělal velký humbuk, u btree se spokojím s oznámením tady na blogu.

GitHub jsem si docela oblíbil, takže opět můžete zdrojový kód získat z repozitáře právě tam:

$ git clone git://github.com/jakubkulhan/btree.git

Nejdříve ze všeho je potřeba si vytvořit instanci. Zase je tu statická metoda open():

$btree = btree::open('my.tree');

Klasicky (alias stejně jako u metatable), pokud vše proběhlo v pořádku, open() vrátí novou instanci btree, jinak FALSE. Proč všude cpeš „oupn“ metodu? Samozřejmě by šlo nechat konstruktor jako public a v případě nějaké chyby vyhodit výjimku. Avšak nemohu si pomoci – výjimky prostě nemám rád. Objekty jsou naprosto skvělé, dokonce i do Céčka (takový ten dřevní jazyk; jen pro fajnšmekry) přenáším některé své návyky z OOP. Ale výjimky? Dílo ďáblovo! Hlavně, když dělají něco, co by člověk nečekal.

Když už jeden má instanci btree, bylo by záhodno si do stromu něco uložit:

$btree->set('key', 'value');

(Opravdu jsem nepřišel na chytřejší název metody.) Narozdíl od metatable může jít jako druhý argument, jako hodnota, cokoli, co se dá serializovat, a ne jen určité typy. Ale stejně jako je tomu u metatable, vymazání určitého klíče se provádí přiřazením NULL:

$btree->set('key', NULL);

Pro získání je jako u metatable metoda get():

assert($btree->get('key') === 'value');

Tohle je všechno, co budete potřebovat, budete-li chtít použít B+Tree. Minimalistická API, to je moje.

U metatable jste se museli starat o uzavírání – buď předáním flagu AUTOCLOSE při otevírání nebo ručně. U btree tomu tak není, protože veškeré úpravy stromu probíhají append-only – tzn. že se nikdy nepřepisují již zapsaná data. Má to hodně výhod. Nemusíte se bát, že by se data nějak poškodila. Prakticky nejsou potřeba zámky (bohužel jsem nevymyslel, jak udělat set() bez zamykání; ale zatímco jedna instance zapisuje, můžete stále v klidu číst pomocí get()). A také to znamenalo velké zjednodušení kódu oproti metatable, kde k zajištění konzistence dat se všechno muselo kopírovat do *~handle souboru.

Co se tedy stane, pokud např. v prostředku zápisu nějakých dat btree vypadne elektřina? Nepůjde to vysvětlit bez toho, abych popsal formát souboru.

Jelikož strom se skládá z jednotlivých uzlů, tak i soubor se musí skládat z uzlů. Navíc se uzly do souboru musí ukládat „ploše“, jelikož soubor je jako jeden dlouhý řetězec jedniček a nul. Každý uzel v souboru je reprezentován jako 32-bitové bezznaménkové číslo (formát N pro pack()) následované počtem bytů uložených právě v tom čísle. První dva byty určují typ uzlu a pak je serializovaná podoba uzlu.

Jsou pouze dva typy uzlů – kp a kv. První typ – kp (key-pointer) – je asociativní pole obsahující dvojice (klíč, ukazatel), kde ukazatel je integer ukazující na jiný uzel (offset uzlu v souboru). Druhý – kv (key-value) – je vždy listem stromu a obsahuje již dvojice (klíč, hodnota). Nebudu nic zapírat, inspirací pro mě byla CouchDB.

CouchDB B+Tree

CouchDB B+Tree (reduce hodnot si nevšímejte, tohle má CouchDB kvůli pohledům a btree žádné pohledy nemá)

„Vždy“ na konci souboru je ukazatel na kořen stromu následovaný „hlavičkou“ sestávající z řetězce "\xffbtree" (\xff je byte plný samých jedniček).

Řekněme tedy, že ukládáme nějakou hodnotu. Jak se tomu u B+Tree dělá, traverzujeme stromem až do listu (kv uzlu), kam by hodnota měla být uložena. Udělá se vše potřebné (rozdělování/slučování uzlů) a postupně se všechny ovlivněné uzly zapisují do souboru – připojují na konec. Na závěr je připojena nová poloha kořene s novou hlavičkou a je vynucen zápis na disk. Když se kdykoli od začátku zápisu do doby, než je všechno úspěšně na disku, něco stane, btree si z toho nic nedělá: Je zapsána třeba jen polovina uzlů? Chybí hlavička? To mě nerozhází!

Při hledání kořene se totiž ověřuje, jestli je na konci souboru opravdu celá hlavička, a pokud ne, btree se od konce souboru dobírá k poslední úspěšně zapsané hlavičce. (Pozor tedy – uložením řetězce "\xffbtree" můžete btree pěkně pomotat hlavu!) To je celé kouzlo. Žádné opravovací algoritmy, nebo žurnály nejsou potřeba, protože i když je soubor „rozbitý“ nedokončeným zápisem, strom je v posledním konzistentním stavu. Všechna data, na která není odkazováno, jsou považována za „neexistující“.

vydáno 21. 8. 2009, 23:15:13

žádný komentář

Zařazeno mezi:

Zkušenosti se Zend_Search_Lucene

Jak se praví v dokumentaci, Zend_Search_Lucene je obecný textový vyhledávací engine napsaný pouze v PHP5. Praktické využití pro většinu webů – fulltextové vyhledávání. Osobně jsem si zvykl spíš používat Google a jeho site:domain.tld. Ale mít vlastní fulltextové vyhledávání je lepší, jelikož Google mě nemusí zas tak rychle zaindexovat a uživatelé jsou na zvyklí.

Zend je celkem moloch, takže nedoporučuji stahovat celý archiv, ale namísto toho využít jen SVN repozitář a provést checkout pouze potřebných adresářů a souborů:

$ mkdir -p lib/Zend
$ svn checkout \
http://framework.zend.com/svn/framework/standard/trunk/library/Zend/Search \
lib/Zend/Search
$ svn cat \
http://framework.zend.com/svn/framework/standard/trunk/library/Zend/Exception.php \
> lib/Zend/Exception.php
$ find lib/ -name ".svn" | xargs rm -rf

Zend potřebuje, aby byla správně nastavena include_path. Je-li framework umístěn v podadresáři lib/, pak je potřeba include_path nastavit nějak takto:

set_include_path(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'lib' .
    PATH_SEPARATOR . get_include_path());

Ještě než se člověk pustí do práce s indexem, je potřeba nastavit locale. Tohle mě docela potrápilo. Na localhostu všechno fungovalo, jak by mělo, ale na produkčním serveru se výsledky hledání nějak podivně omezily jen na pár věcí – jako by se nezaindexovalo posledních pár slov. Zkusil jsem vše přeindexovat asi 3krát, ale pořád to samé. Kdyby mě rovnou napadlo se podívat do chybového logu, mohl jsem serveru ušetřit hodně procesorového času. Nejdříve jsem si logu vůbec nevšímal, ale jak dosáhl velikosti někde kolem 3 MiB, přilákal tím tak trochu mou pozornost. Problém byl v jiném nastavení locale u mě doma a na produkčním serveru, takže iconv() skončilo s Notice o nepovoleném znaku, čímž se zpracovala jen část řetězce. Doma jsem měl nastaveno UTF-8, na produkčním je asi ISO-8859-2, takže setlocale():

setlocale('cs_CZ.UTF-8');

Fulltext jsem potřeboval pro jednu instanci Shopaholic. Nakonec jsem se rozhodl, že indexovat se budou akorát produkty – u e-shopu lidi stejně nic jiného nezajímá (alespoň doufám). (Jelikož jsem na této instanci provedl ještě nějaké úpravy, které by nebylo dobré zahrnout do hlavní větve, bude chvilku trvat, než se fulltext dostane do repozitáře na GitHubu.)

Celé indexování se Zend_Search_Lucene probíhá ve vytváření dokumentů a jejich ukládání do indexu. Každý dokument obsahuje různá pole, pomocí kterých se dá vyhledávat. Jedno pole je pro vyhledávání výchozí, a tak se při psaní dotazu nemusí uvádět jeho jméno.

Pole jsou různých typů a mají různé vlastnosti. Pro aplikace, kde slouží jako persistentní úložiště databáze, je podle mě nejlepší zvolit pro všechna pole typ UnStored – bylo by zbytečné data z databáze ještě replikovat v indexu. Jediné, co Shopaholic ukládá do indexu, je ID produktu, podle kterého se poté při vyhledávání podnikne dotaz na databázi a vyberou skutečná data.

Když se na to podíváme z toho pohledu, že data jsou v databázi, pak možnosti indexování jednotlivých polí ztrácí význam. Řekněme, že název produktu je ukládán do pole name výrobce do manufacturer a popis produktu do description. Jako výchozí se zvolí pole description. Ale teď chce zákazník vyhledat všechny produkty od výrobce foobar. Aby tak mohl učinit, musel by do vyhledávacího políčka zadat manufacturer:foobar. Ale kdo tohle udělá? Zákazník chce prostě zadat foobar a mít výrobky od společnosti foobar.

Takže všechny informace, které by mohl chtít zákazník hledat, je nakonec potřeba dát do jednoho pole a to nastavit jako výchozí. Zvažuji, jestli do hlavní větve ponechat i další pole (kvůli pokročilejším dotazům), nebo se jich úplně zbavit a nechat jen jedno jediné? Spíš zbavit, Shopaholic má směřovat k co největší jednoduchosti – easy yet powerful, a ne powerful yet easy (protože to stejně nikdy pak easy není).

Potom tu vyvstává problém, jak aktualizovat index, když se některý z produktů změní. Jsou dvě možnosti, každá má svá pro a proti:

  1. aktualizovat index hned při jakékoli změně produktu

  2. vyčlenit změny zvlášť

První možnost přidá, a to docela dost, na čase všech operací, takže práce s produkty je pomalejší. Ale zase zachovává index neustále aktuální a zákazník vždycky najde to, co je právě teď v databázi. U druhého řešení je to naopak – práce s produkty je plynulejší, ale zákazník nemusí vždy najít to, co vlastně chtěl, protože se to mezitím mohlo změnit. A taktéž přidává nutnost do rozhraní nějak tu volbu k aktualizaci fulltextu zapracovat. Vzhledem k importu nakonec vyhrála druhá možnost.

Jak ukládat informace o tom, že produkt změnil? Databázové schéma postrádá informace o tom, kdy byl produkt vložen, či upraven (jaké to špatné mé návrhové rozhodnutí!), takže tudy cesta nevede. I když teď mě napadá, že by se to dalo sledovat pomocí změn cen – při každém ukládání produktu je zároveň zapsána cena, s kterou je v ten okamžik ukládán. Proč mě to nenapadlo dříve?

Vyřešil jsem to tedy jinak – bitmapou (a teď myslím tu bitmapu, tu strukturu, ne obrázek) ukládanou do souboru. Kód na konci článku.

Zend_Search_Lucene mě potěšilo – jak svou jednoduchostí, tak rychlostí. Originál Lucene je asi rychlejší, ale na PHP to podle mě vůbec není špatné. Taky mě to utvrdilo v tom, že Zend má své dobré části – Zend_Search_Lucene, různá API –, ale celkově je to moloch a jako základ k psaní aplikací bych ho nepoužil.

function bitmap($filename)
{
    // get all true
    if (func_num_args() === 1) {
        if (($data = file_get_contents($filename)) === FALSE) return NULL;
        $bits = array();
        for ($i = 0, $len = strlen($data); $i < $len; ++$i) {
            $byte = ord($data{$i});
            for ($j = 0; $j < 8; $j++) 
                if ($byte & (1 << (7 - $j))) $bits[] = $i * 8 + $j;
        }

        return $bits;

    // get/set bit
    } else {
        $bit = intval(func_get_arg(1));
        $set = NULL;
        if (func_num_args() > 2) $set = func_get_arg(2);
            
        // open file
        if (!($handle = @fopen($filename, 'r+b'))) return NULL;
        if (!flock($handle, $set === NULL ? LOCK_SH : LOCK_EX)) {
            fclose($handle);
            return NULL;
        }

        // read byte with needed bit
        $offset = intval(floor($bit / 8));
        $byte_offset = 7 - ($bit % 8);
        if (fseek($handle, $offset, SEEK_SET) === -1) {
            fclose($handle); 
            return NULL;
        }
        $byte = ord(fread($handle, 1));

        // get
        if ($set === NULL) {
            fclose($handle);
            return (bool) ($byte & (1 << $byte_offset));
        }

        // set
        $byte = ($a = ($byte >> ($byte_offset + 1) << ($byte_offset + 1))) |
            $byte & ~($a + (1 << $byte_offset)) |
            intval((bool) $set) << $byte_offset;

        if (fseek($handle, $offset, SEEK_SET) === -1) {
            fclose($handle);
            return NULL;
        }

        if (fwrite($handle, chr($byte), 1) !== 1) {
            fclose($handle);
            return NULL;
        }

        fflush($handle);
        fclose($handle);
        return TRUE;
    }
}

vydáno 28. 7. 2009, 15:02:51

2 komentáře (2 nové)

Zařazeno mezi: