Пре­жде чем на­чать оче­ред­ное по­вест­во­ва­ние, ко­то­рое бу­дет аб­со­лют­но неин­терес­но лю­би­те­лям сме­хуе­чек (де­ле­га­ция фиш­ки­нет вста­ла и мол­ча вы­шла из за­ла) - не­большое ли­ри­че­ское от­ступ­ле­ние. Мне на мы­ло на­пи­сал чу­вак с за­бав­ным во­про­сом, мол где я, ве­зу­чий су­кин сын, на­хо­жу столь­ко проек­тов под ма­ло­из­вест­ный и не­рас­про­стра­нен­ный но­де. Отве­чаю: проек­тов под эту плат­фор­му прак­ти­че­ски нет, язы­ком про­грам­миро­ва­ния это на­звать не­льзя, это ско­рее server-side runtime environment для javascript, инстру­мен­та­рий для ре­ше­ния опре­де­лен­но­го кру­га за­дач. Ме­ня на­хо­дят со­вер­шен­но по дру­гим кри­те­ри­ям (мой рост, со­от­но­ше­ние раз­ме­ра ступ­ни к диа­мет­ру зрач­ка, мо­гу го­во­рить, как Ста­ло­не, ну вы по­ня­ли), я лишь вы­би­раю NodeJS для ре­а­ли­за­ции. А мог бы вы­би­рать ер­лан­говские Mochiweb, Misultin, Cowboy (по­ка­зы­ва­ю­щие да­же бо­лее бы­стрые ре­зульта­ты) или пайто­новский Tornadoweb, но мне род­ной JS как-то бли­же к те­лу, к то­му же под не­го есть от­лич­ный ре­по­зи­та­рий го­то­вых ве­ло­си­пе­дов на все слу­чаи жиз­ни под на­зва­ни­ем NPM. Хо­тя, воз­мож­но, в бли­жайшем бу­ду­щем тут по­явят­ся по­сты и про вы­ше­на­зван­ные тех­но­ло­гии. Ин­терес­но? Жми на +1.

Итак, как ме­ня учи­ли в гу­ма­ни­тар­ном ВУЗе, на­чнем с по­ста­нов­ки пробле­ма­ти­ки. Ко мне обра­тил­ся мой зна­ко­мый, под­ряд­чик од­ной круп­ной фир­мы в Бри­та­нии со сле­ду­ю­щим во­про­сом: есть 40 GB до­ку­мен­тов в HTML фор­ма­те (при бли­жайшем рассмот­ре­нии, раз­мер со­кра­тил­ся до 18 GB, что то­же не­ма­ло, столь­ко при­мер­но ве­сит html дамп рус­скоязыч­ной ви­ки­пе­дии со­сто­я­ни­ем на 2008 год), ин­фор­ма­ция крайне кон­фи­ден­ци­аль­ная (днев­ни­ки прин­цесс и ко­ро­лей, ага) и со­би­ра­лась за сто­лет­нюю ис­то­рию компа­нии, лет 5-6 то­му на­зад фир­ма пере­шла на ка­кую-то свою си­сте­му хра­не­ния до­ку­мен­та­ции, но ста­рый дамп остал­ся ста­тич­ным и тре­бо­вал ин­дек­са­ции для по­ис­ка по не­му. Ин­тра­нет не поз­во­лял ис­поль­зо­вать Google, им­пор­ти­ро­вать все в но­вую си­сте­му до­ку­мен­тоо­бо­ро­та ни­кто не хо­тел, ко­му ну­жен ин­фор­ма­ци­он­ный му­сор при ка­кие-то ре­зульта­ты са­ни­тар­ных ана­ли­зов во­ды в Нью-Гэмп­ши­ре за 1928 год? Нуж­но отдать долж­ное со­труд­ни­кам этой конто­ры, они усерд­но оциф­ро­вы­ва­ли все и толко­во со­став­ля­ли ка­та­ло­ги вруч­ную, а зна­чит к каж­до­му до­ку­мен­ту был до­ступ по ка­кой-то ги­пер­с­сыл­ке с дру­гой стра­ни­цы. Го­то­вые ре­ше­ния я да­же не ис­кал по опре­де­лен­ным тех­ни­че­ским при­чи­нам, бы­ло при­ня­то ре­ше­ние де­лать свой crawler + indexer, что в на­ше вре­мя ока­за­лось впол­не три­виаль­ной за­да­чей.

Са­мо­го па­у­ка, ко­то­рый бу­дет пу­те­ше­ство­вать по пыль­но­му ар­хи­ву, на­пи­сать, как два паль­ца об ас­фальт, об этом мы по­го­во­рим чу­точ­ку поз­же, оста­но­вим­ся луч­ше на тех­но­ло­гии по­ис­ка и хра­не­ния ин­дек­са. Вся­кие ре­ля­ци­он­ные ба­зы дан­ных отпа­ли сра­зу по­сле то­го, как я вы­пол­нил du -sh в ди­рек­то­рии с ка­та­ло­гом. Хо­тел бы­ло по­про­бо­вать сде­лать что-то свое на осно­ве NoSQL, но из всех из­вест­ных мне движ­ков не поз­во­лял мне ис­кать подоб­ным об­разом (чуть поз­же я слу­чай­но на­т­кнул­ся на Riak, увы, бы­ло уже позд­но), толь­ко MongoDB мог по­хва­стать­ся под­держ­кой ре­гу­ля­рок при вы­бор­ке, а про пол­но­цен­ный же full text и речь не шла, в офи­ци­аль­ном ма­нуале со­ве­то­ва­ли раз­би­вать текст на клю­че­вые сло­ва, но это ужас­ное ре­ше­ние. Имен­но поэто­му бы­ло при­ня­то ре­ше­ние хо­тя бы здесь не изоб­ре­тать двух­ко­лес­ные ме­ха­низ­мы и ис­поль­зо­вать го­то­вые удоб­ные ре­ше­ния. Я успел по­смот­реть на Apache Solr и ElasticSearch, ко­то­рые по­строе­ны на Java Lucene и на Sphinx. По­след­ний сра­зу отпал, так как его пре­иму­ще­ства в ви­де не­нуж­ных мне ин­дек­са­ции SQL баз дан­ных и сво­е­го Query Language для слож­ных вы­бо­рок бы­ли све­де­ны на нет необ­хо­ди­мо­стью пи­сать отдель­ную XML pipe для до­бав­ле­ния ста­ти­ки. Solr, име­ю­щий ве­ли­ко­леп­ный адми­ни­стра­тив­ный ин­тер­фейс, большое ком­мью­ни­ти, воз­мож­ность ин­дек­си­ро­вать DOC и PDF (ну мы и са­ми мо­жем ис­поль­зо­вать Apache Tika в лю­бом дру­гом движ­ке) по­ка­зал­ся ка­ким-то слиш­ком слож­ным для бы­стро­го стар­та и из­бы­точ­ным, плюс ему не хва­та­ло внят­ной до­ку­мен­та­ции (до­ки в wiki это FFFUUUU). По­это­му я и оста­но­вил­ся на ElasticSearch, ко­то­рый мо­жет по­хва­стать­ся ве­ли­ко­леп­ным REST API на осно­ве JSON, рус­ской мор­фо­ло­ги­ей из ко­роб­ки, раз­лич­ны­ми тек­сто­вы­ми ана­ли­за­то­ра­ми и воз­мож­но­стью со­че­тать их в лю­бой по­сле­до­ва­тель­но­сти, ли­бо да­же со­зда­вать свои соб­ствен­ные, встроен­ны­ми язы­ка­ми скрип­то­ва­ния (js или python) для вы­бор­ки и фильтра­ции, мно­ги­ми ва­ри­ан­та­ми storage для на­ше­го ин­дек­са, ку­чей па­ра­мет­ров сор­ти­ров­ки и ран­жи­ро­ва­ния, под­све­чи­ва­ни­ем ре­зульта­тов, wildcard запро­са­ми и AND, OR клю­че­вы­ми сло­ва­ми, про­стым со­зда­ни­ем кла­стера и ре­пли­ка­ци­ей меж­ду но­да­ми, да­же воз­мож­ность ис­поль­зо­ва­ния по­ис­ко­во­го движ­ка в ка­че­стве key-value хра­ни­ли­ща дан­ных при пра­виль­ном ма­ппин­ге. И да, все ра­бо­та­ет без ма­лей­шей кон­фи­гу­ра­ции в сти­ле load`n`run, я толь­ко из­ме­нил network.host на 127.0.0.1 и по­сле запус­ка по­лу­чил ра­бо­та­ю­щее хра­ни­ли­ще на­ше­го ин­дек­са. А еще у ела­сти­ка есть, если и не иде­аль­ная, то хо­тя бы внят­ная до­ку­мен­та­ция и кру­тое Java API, ко­то­рое, впро­чем, нам не по­на­до­бит­ся.

Те­перь мо­жем пере­хо­дить не­по­сред­ствен­но к на­пи­са­нию на­ше­го crawler-а, ко­то­рый бу­дет пере­хо­дить по ссыл­кам и от­прав­лять дан­ные в ElasticSearch. Пре­жде всего, я на­пи­сал свою оберт­ку во­круг RESTа ела­сти­ка для об­лег­че­ния про­цес­са ин­дек­са­ции и по­ис­ка, она очень про­стая и ре­а­ли­зу­ет толь­ко функ­ци­о­нал двух функ­ций - index (плюс update) и search. Код об­вяз­ки мож­но по­смот­реть. Его мы бу­дем ис­поль­зо­вать, как в на­шем по­ис­ко­вом бо­те, так и в сер­вер­ном при­ло­же­нии, воз­вра­ща­ю­щем от­вет поль­зо­ва­те­лю. Те­перь не­по­сред­ствен­но о пре­иму­ще­ствах NodeJS в этой сфе­ре. Пре­жде всего, это мульти­по­точность из ко­роб­ки, ра­зу­ме­ет­ся, на всех совре­мен­ных язы­ках мож­но до­бить­ся ана­ло­гич­но­го ре­зульта­та (ну раз­ве что кро­ме по­ха­пе, но оно там аб­со­лют­но не нуж­но); и, вто­рой по зна­чи­мо­сти плюс, - jQuery. Да, я не опе­ча­тал­ся, мы бу­дем ис­поль­зо­вать се­лек­то­ры и мо­ди­фи­ка­то­ры jquery на сер­вер­сайде. Все это воз­мож­но благо­да­ря ве­ли­ко­леп­ной биб­лио­те­ки JSDOM, ко­то­рая поз­во­ля­ет нам по­лу­чить вир­ту­аль­ный DOM из ко­да HTML раз­мет­ки и эму­ли­ро­вать все вы­зо­вы к не­му. По­че­му jQuery? Ответ прост - ба­наль­ная лень в ущерб опре­де­лен­ной произ­во­ди­тель­но­сти, ко­неч­но же мы мо­жем ис­поль­зо­вать ре­гу­ляр­ки для вы­бор­ки всех ссыл­кок, но $('a').each() вы­гля­дит про­ще и сим­па­тич­ней. Столк­нул­ся с необыч­ной пробле­мой, ко­то­рая опи­са­на в том чис­ле и в ко­де само­го крауле­ра: ElasticSearch по­че­му-то наот­рез иг­но­ри­ро­вал точ­ки, двое­то­чия, слешы и про­чие сим­во­лы, поэто­му сде­лать про­верку ста­ту­са ин­дек­са­ции ссыл­ки че­рез ее url не по­лу­чи­лось, для этой це­ли ис­поль­зу­ет­ся md5 хэш. В це­лом, по­лу­чи­лось до­ста­точ­но бы­строе ре­ше­ние, дан­ный блог, скорм­лен­ный сво­ей глав­ной стра­ни­цей на­ше­му бо­ту, был пол­но­стью до­бав­лен в кеш ела­сти­ка за пол­то­ры ми­ну­ты всего-то в 3 по­то­ка. Я ста­рал­ся, по ме­ре воз­мож­но­сти, ком­мен­ти­ро­вать ис­ход­ный код, на­де­юсь, что у вас не воз­ник­нет проблем с его чте­ни­ем. В иде­а­ле, я со­ве­тую ис­поль­зо­вать 10 по­то­ков с queueInterval око­ло 100, если ва­ши се­те­вая под­си­сте­ма и про­цес­сор вы­дер­жат подоб­ные вы­со­кие на­груз­ки, с та­ки­ми зна­че­ни­я­ми мне уда­ва­лось ин­дек­си­ро­вать око­ло трех­сот стра­ниц ви­ки­пе­дии в ми­ну­ту на сред­нень­ком же­ле­зе и ка­на­ле в 30 mbit\s. Па­ук про­ве­ря­ет вхо­жде­ние до­ме­на ин­дек­си­ру­е­мой ссыл­ки в раз­ре­шен­ный спи­сок и ищет доз­во­лен­ный content-type в от­ве­те, ре­а­ли­зу­ет свое­об­разный robots.txt при по­мо­щи мас­си­ва ре­гу­ля­рок indexQueryPath. К то­му же воз­мож­на пере­ин­дек­са­ция при до­сти­же­нии опре­де­лен­но­го воз­рас­та ин­дек­са, у ме­ня, по умол­ча­нию, это 100 дней. Про­цесс ин­дек­са­ции в ва­шей кон­со­ли бу­дет вы­гля­деть как-то так. Ра­бо­та­ю­щий скрипт в memory leaks за­ме­чен не был, но вот JSDOM и jQuery очень лю­бят про­цес­сор, будь­те го­то­вы к вы­со­ким на­груз­кам. Для де­монстра­ции я про­шер­стил этот бло­жек и на­пи­сал не­большое при­ло­же­ние по­ис­ка по всем по­стам, со­сто­я­щее из та­кой вот сер­вер­ной ча­сти. Его ра­бо­ту мож­но по­смот­реть в ла­бо­ра­то­рии им. Бе­на Ган­на по ссыл­ке.

В за­клю­че­ние, хо­чу вы­ра­зить благо­дар­ность раз­ра­бот­чи­кам и со­об­ще­ству NodeJS и ElasticSearch. При всей мо­ей не­лю­бви к крас­но­гла­зо­му opensource, два этих ве­ли­ко­леп­ных про­грамм­ных про­дук­та, со­зда­ва­е­мых на чи­стом эн­ту­зи­аз­ме, по­мо­гли мне сде­лать ра­бо­ту в срок, об­ра­до­вать вы­чур­ных бри­тан­цев и ку­пить но­вую до­зу ге­рои­на. Очень слож­но за­тя­ги­вать жгут на ру­ке и пи­сать сю­да, до но­вых встреч, дру­зья.