Pełnotekstowe wyszukiwanie w PHP z Solr i Lucene

Solr to serwer pełnotekstowego wyszukiwania oparty o Lucene. Solr poprzez interfejs XML/JSON/HTTP umożliwia indeksowanie, wyszukiwanie, aktualizowanie i kasowanie danych. Zastosowana biblioteka Lucene zapewnia bogaty zestaw funkcjonalności. Zarówno Lucene jak i Solr napisane są w Javie, lecz niezależny od języka interfejs umożliwia korzystanie z serwera z poziomu praktycznie każdego języka programistycznego. Jeżeli rozwiązania takie jak Sphinx stają się zbyt "słabe" to Solr może okazać się dobrą alternatywą.

Zalety Solr

  • Wieloplatformowy i łatwo dostępny (Java, REST)
  • Aktywnie rozwijany (Apache Foundation)
  • Replikacja, import CSV, obsługa synonimów, podświetlanie trafień
  • Szybki i wydajny serwer
  • Używany m.in. przez digg.com, wikipedię, sourceforge, gamespot
Wadą jest fakt iż to 100% Javy i wymaga serwera servletów i całej "infrastruktury" oprogramowania związanej z nią.

Instalacja i uruchomienie Solr

  • By solr działał potrzebna jest zainstalowana wirtualna maszyna Javy
  • Pobieramy najnowszą wersję Solr
  • Rozpakuj archiwum i przjedź do apache-solr-*/example i wykonaj polecenie:
    java -jar start.jar
  • Jeżeli wszystkie zależności są spełnione i nie wystąpią nieoczekiwane problemy to serwer Solr zbudowany z serwerem Jetty powinien się uruchomić. Panel admin Solr powinien być dostępny pod adresem http://0.0.0.0:8983/solr/admin/.
    solr1

Indeksowanie i wyszukiwanie

Schemat indeksu

Do tworzenia określonych indeksów Solr używa schematów określających strukturę i sposób obsługi indeksowanych danych. Oto przykład prostego schematu:
<fields>
  <field name="id" type="string" indexed="true" stored="true" required="true" />
  <field name="title" type="text" indexed="true" stored="true"/>
  <field name="description" type="text" indexed="true" stored="true"/>
  <field name="text" type="text" indexed="true" stored="true"/>
  </fields>
  <uniqueKey>id</uniqueKey>
Określamy pola nadając im nazwy, typ, a także dodatkowe parametry jak indexed określające czy pole ma być indeksowane, stored daje możliwość pobierania wartości danego pola w wynikach wyszukiwania.

Edytuj solr/conf/schema.xml i znajdź:
 <fields>
   <!-- Valid attributes for fields:
     name: mandatory - the name for the field
     type: mandatory - the name of a previously defined type from the <types> section
     indexed: true if this field should be indexed (searchable or sortable)
     stored: true if this field should be retrievable
     compressed: [false] if this field should be stored using gzip compression
       (this will only apply if the field type is compressable; among
       the standard field types, only TextField and StrField are)
     multiValued: true if this field may contain multiple values per document
     omitNorms: (expert) set to true to omit the norms associated with
       this field (this disables length normalization and index-time
       boosting for the field, and saves some memory).  Only full-text
       fields or fields that need an index-time boost need norms.
   -->

.....
 <uniqueKey>id</uniqueKey>
Zastąp całość:
<fields>
  <field name="id" type="string" indexed="true" stored="true" required="true" />
  <field name="title" type="text" indexed="true" stored="true"/>
  <field name="description" type="text" indexed="true" stored="true"/>
  <field name="text" type="text" indexed="true" stored="true"/>
  </fields>
  <uniqueKey>id</uniqueKey>
Znajdź i usuń definicje starego schematu:
   <copyField source="id" dest="sku"/>

   <copyField source="cat" dest="text"/>
   <copyField source="name" dest="text"/>
   <copyField source="name" dest="nameSort"/>
   <copyField source="name" dest="alphaNameSort"/>
   <copyField source="manu" dest="text"/>
   <copyField source="features" dest="text"/>
   <copyField source="includes" dest="text"/>

   <copyField source="manu" dest="manu_exact"/>
Zrestartuj serwer Solr.

Dodawanie i aktualizowanie danych

Aby dodać dane należy wysłać POSTem pod /solr/update/ dane w postaci XML:
<add><doc>
<field name="nazwapola">wartość</field>
<field name="nazwapola">wartość</field>
<field name="nazwapola">wartość</field>
</doc></add>
Oczywiście elementów ("doc") może być więcej niż jeden. Aktualizacja następuję w momencie wysłania dokumentu o istniejącej już w indeksie wartości unikalnego pola (zazwyczaj "id"). Dodatkowo by wszystkie zmiany "weszły w życie" należy wykonać operację wysyłając
<commit/>

Wyszukiwanie

By pobrać wyniki wyszukiwania należy wykonać żądanie GET adresu /solr/select z parametrami takimi jak:
  • q=NAZWA_POLA:SZUKANA_FRAZA - szukanie podanej frazy w danych z podanego pola
  • fl=POLE1,POLE2,POLEn - jakie pola mają być zwrócone wraz z wynikami

Kasowanie rekordów z indeksu

Aby usunąć rekord należy wysłać POSTem pod /solr/update/ dane w postaci XML:
<delete>
<id>WARTOŚC</id>
</delete>
Gdzie id to pole o unikalnej wartości. Istnieje możliwość też kasowania dla różnych warunków, a nie pojedynczych rekordów.

Obsługa Solr z poziomu PHP

Dostępne jest już binarne rozszerzenie do obsługi Solr zapewniajace lepsze i wydajniejsze API.
Istnieje gotowa biblioteka obsługująca serwer Solr - ze strony projektu pobieramy SolrPhpClient.zip i rozpakowujemy go do pustego katalogu "Apache" tak by otrzymać ścieżkę Apache/Solr/Service.php. Biblioteka wymaga PHP5 (najlepiej 5.2.0, koniecznie z funkcją json_decode). Oto przegląd jej podstawowych możliwości.
<?php
require_once( 'Apache/Solr/Service.php' );
# łączymy się z serwerem
$solr = new Apache_Solr_Service( '0.0.0.0', '8983', '/solr' );
if ( ! $solr->ping() ) {
  echo 'Solr nie odpowiada.';
  exit;
}

#dodanie do indeksu
  $parts = array(
    'test1' => array(
      'id' => 22,
      'title' => 'Search engines',
      'description' => 'Opis',
      'text' => 'treść',
    ),
    'test2' => array(
       'id' => 21,
      'title' => 'Searching a database',
      'description' => 'Opis2',
      'text' => 'treść2',
    )
  );

  $documents = array();

  foreach ( $parts as $item => $fields ) {
    $part = new Apache_Solr_Document();

    foreach ( $fields as $key => $value ) {
      if ( is_array( $value ) ) {
        foreach ( $value as $datum ) {
          $part->setMultiValue( $key, $datum );
        }
      }
      else {
        $part->$key = $value;
      }
    }
    
    $documents[] = $part;
  }

# przesłanie dokumentów do Solr
  try {
    $solr->addDocuments( $documents );
    $solr->commit();
    $solr->optimize();
  }
  catch ( Exception $e ) {
    echo $e->getMessage();
  }

# Wyszukiwanie
  $offset = 0;
  $limit = 10;

# szukamy frazy "search" w polu "title"
  $queries = array(
    'title: search'
  );

  foreach ( $queries as $query ) {
    $response = $solr->search( $query, $offset, $limit );
    
    if ( $response->getHttpStatus() == 200 ) {
      // print_r( $response->getRawResponse() );
      
      if ( $response->response->numFound > 0 ) {
        echo '<h1>'.$query.'</h1>';

        foreach ( $response->response->docs as $doc ) { 
          echo $doc->id.' - '.$doc->title.'<br />';
        }
        
        echo '<br />';
      }
    }
    else {
      echo $response->getHttpStatusMessage();
    }
  }
?>
Można też napisać własną mniejszą bibliotekę wysyłającą żądania POST i GET do serwera Solr. W przypadku dodawania/modyfikowania danych należy pamiętać że dane (XML) wysłane muszą być jako treść (body), a nie jako dane POST.

W sieci

Enterprise search with PHP and Apache Solr
Search smarter with Apache Solr, Part 1: Essential features and the Solr schema
Search smarter with Apache Solr, Part 2: Solr for the enterprise
Prezentacje na slideshare
RkBlog

PHP w Akcji, 29 July 2008

Comment article
Comment article RkBlog main page Search RSS Contact