Showing posts with label fulltext search. Show all posts
Showing posts with label fulltext search. Show all posts

Sunday, December 18, 2016

Implementasi Full-text Search dengan ElasticSearch dan Firebase

Kembali lagi pada sesi pembahasan mengenai implementasi Firebase pada blog Nostra ini. Seperti yang pernah dibahas pada beberapa tulisan sebelumnya, Firebase merupakan sebuah layanan Backend as a Service (Baas) yang sudah diakuisisi oleh Google. Oke, kalau Firebase merupakan layanan BaaS, lalu apakah kita masih perlu sebuah server dengan Backend yang kita kembangkan sendiri? Jawabannya adalah tergantung pada desain/arsitektur aplikasi yang dikembangkan.

Pada dasarnya Firebase dapat diimplementasikan dengan tiga pilihan cara, sesuai dengan tulisan pada artikel milik Firebase. Berdasarkan artikel tersebut, bagaimana caranya untuk mengimplementasikan ElasticSearch (ES) bersamaan dengan Firebase? dan apa sebenarnya ES itu sendiri? Pada tulisan ini, kami akan mencoba untuk menjelaskan secara singkat mengenai ES dan bagaimana mengimplementasikannya dengan Firebase.

Pertama mari kita bahas terlebih dahulu mengenai ES. Secara sederhana ES adalah sebuah open source search engine yang dibangun diatas Apache Lucene. ES juga dapat dikatakan sebagai sebuah RESTful search engine, karena ES berkomunikasi dengan menggunakan RESTful API. Secara arsitektur ES terdiri atas node dan cluster sehingga ES sebaiknya dipasang pada Virtual Machine (VM) tersendiri, sehingga ES dapat memiliki performa maksimal. Apakah sudah terbayang mengenai apa itu ES? Jika belum, maka sebaiknya melakukan eksperimen dengan mengikuti panduan resmi milik ES.

Apabila ES dipasang sebagai sebuah VM tersendiri, lalu bagaimana ES dapat diimplementasikan bersama dengan Firebase? berdasarkan artikel yang telah diberikan pada bagian awal tulisan ini, maka implementasi tersebut berada pada arsitektur kedua, yang dapat dilihat pada gambar berikut.


Lalu dengan arsitektur tersebut, dimana letak VM dari ES seperti yang telah disebutkan?
.
.
.
.
.
.
.
Benar sekali, berdasarkan arsitektur tersebut maka tidak terdapat VM terpisah dari ES, melainkan ES dipasang langsung pada VM yang sama dengan server. Apabila menggunakan arsitektur tersebut maka ES dan Backend (BE) dari aplikasi kita akan berada pada satu VM yang sama. Hal tersebut sebaiknya dihindari, kenapa? Karena apabila salah satu dari ES ataupun BE aplikasi kita mengalami workload yang berlebihan, maka hal tersebut akan saling mengacaukan performa satu sama lainnya. Oleh karena itu sebaiknya ES dipasang pada VM yang terpisah.

Lalu?
.
.
.
.
.
.
.
Jika kita mengimplementasikan BE dan ES pada dua VM yang berbeda, maka arsitektur dari aplikasi kita kurang lebih akan seperti pada gambar berikut.


Berdasarkan arsitektur tersebut, maka ES akan berkomunikasi secara langsung dengan BE, sedangkan BE juga akan berkomunikasi dengan Firebase. Mengapa kita menggunakan arsitektur model kedua pada artikel diatas? Kenapa bukan arsitektur ketiga? Good questions.

Alasannya adalah karena pada implementasi ini BE akan bersikap sama seperti Fronted (FE), yaitu BE akan me-listen pada node di Firebase. Dengan implementasi tersebut maka kita akan menyimpan konfigurasi search query dan juga search request serta search result pada Firebase Real-time Database seperti pada contoh di bawah.


Seperti yang dapat dilihat, secara realtime Firebase akan mencatat search request yang selanjutnya akan dibaca oleh BE dan kemudian dilemparkan kepada ES untuk melakukan pencarian. Implementasi tersebut dapat dilakukan dengan beberapa tahapan, antara lain:

1. Sinkronisasi data antara Firebase dengan ES yang dapat dilakukan dengan memanfaatkan listener terhadap node yang akan dilakukan pencarian, seperti pada contoh berikut.

2. Memasang listener pada node search request, sehingga setiap ada input pencarian maka BE akan langsung melemparkan query tersebut pada ES untuk mencari data yang dimaksud. Contoh implementasi pada tahap ini dapat dilihat pada gambar berikut.

3. Setelah terdapat pencarian pada node search request, maka langkah selanjutnya adalah melakukan proses pencarian pada ES berdasarkan masukan pada node tersebut. Karena search query sudah kita simpan pada Firebase Real-time Database, maka implementasi pencarian pada ES dapat kita implementasikan seperti pada contoh berikut.

4. Ketika proses pencarian pada ES telah selesai, maka BE akan mengembalikan hasil pencarian ES pada node search result di Firebase seperti pada contoh implementasi berikut.

5. Selanjutnya pada sisi Frontend (FE) juga akan memasang listener pada node search result sehingga ketika BE mengembalikan hasil pencarian pada Firebase maka FE akan langsung mendapatkan hasil tersebut.


Bagaimana? semoga tulisan ini dapat membantu kalian yang ingin mencoba untuk mengimplementasikannya. Good luck and have a nice app!


Sampai berjumpa pada tulisan-tulisan berikutnya.






P.S.
Contoh implementasi lain dari ElasticSearch dengan Firebase dapat dilihat pada link ini.


Referensi:
https://www.elastic.co/guide/en/elasticsearch/guide/current/intro.html
https://qbox.io/blog/what-is-elasticsearch
http://stackoverflow.com/questions/671118/what-exactly-is-restful-programming

Thursday, June 18, 2015

FullText Search Query : menggunakan Hibernate-Search


Jumpa lagi kita dengan team nostratech. Kali ini kita akan belajar tentang seraching data di database menggunakan Full Text Search. Apa itu full text search?.

Seperti saat kita search di google. Kita ketik keyword apa, maka akan muncul list data yang sesuai (match atau sama persis dengan keyword yang kita ketik) dan juga semua data ada hubungannya dengan keyword yang kita ketik. Misal kita ketik 'hibernate book', maka akan muncul semua list yang ada kata mirip denhan 'hibernate' dan 'book'.



Seperti itulah sederhananya pengertian full text search.

Kita ambil contoh yang sederhana saja.
Misalnya kita punya data list nama contact di tabel seperti ini :


Disini kita asumsikan kita menggunakan ORM hibernate.

Bagaimana caranya search data “Paolo Coelho Jr” ? Kalo menggunakan keyword 'paolo' tentunya kita memakai query :

 select a from Contact where a.name like 'paolo%'  

jika keyword dari usernya 'coelho' :

 select a from Contact where a.name like '%coelho%'  

Nah bagaimana jika keyword dari usernya seperti ini : 'coelho Jr', 'coelho jr paolo', 'famous writer paolo coelho', 'the name of Coelho NM paolo ' dan lain lain.
Tentu kita akan bingung dan akan kesulitan tentunya kita.
Search query classic : name like '%keyword%' tentu tidak bisa menghandle lagi jika sudah menggunakan query yang aneh aneh seperti diatas.
Selain tidak mendapatkan hasil yang diharapkan, tentunya query %% tentu sangat buruk untuk masalah performence kalau datanya sudah terlampau banyak (walaupun field name di tabel Contact sudah di Index).

Solusinya kita bisa menggunakan library hibernate-search untuk menangani search query untuk problem diatas.
Hibernate search didalamnya menggunakan engine apache lucene untuk indexing datanya. Jadi nantinya data Name dari tabel Contact akan disimpan di index lucene.
Hibernate tidak akan langsung mencari data di tabel contact tetapi di cari terlebih dulu di index luncene.

Proses yang terjadi kurang lebih seperti ini :

Hibernate --> search index lucene --> search index name dan get id nya --> search ke tabel contact berdasarkan id --> tampilkan data.

Tambahkan dependency maven di pom.xml

 <dependency>  
   <groupId>org.hibernate</groupId>  
   <artifactId>hibernate-core</artifactId>  
   <version>4.3.9.Final</version>  
 </dependency>  
 <dependency>  
   <groupId>org.hibernate</groupId>  
   <artifactId>hibernate-search-orm</artifactId>  
   <version>5.2.0.Final</version>  
 </dependency>  

Mari kita mulai ke contohnya.

Kita buat dulu model Contact.

Set property mana yang akan diindex menggunakan @Field

 import javax.persistence.Entity;  
 import javax.persistence.Id;  
 import javax.persistence.Table;  
 import org.apache.lucene.analysis.core.LowerCaseFilterFactory;  
 import org.apache.lucene.analysis.snowball.SnowballPorterFilterFactory;  
 import org.apache.lucene.analysis.standard.StandardTokenizerFactory;  
 import org.hibernate.search.annotations.*;  
 /**  
  * The persistent class for the contact database table.  
  */  
 @Entity  
 @Indexed  
 @AnalyzerDef(name = "customanalyzer",  
     tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),  
     filters = {  
         @TokenFilterDef(factory = LowerCaseFilterFactory.class)  
         , @TokenFilterDef(factory = SnowballPorterFilterFactory.class  
     )  
     })  
 @Table(name = "contact")  
 public class Contact {  
   @Id  
   @DocumentId  
   private Integer id;  
   @Field(index = Index.YES, store = Store.NO)  
   @Analyzer(definition = "customanalyzer")  
   private String name;  
   private String email;  
      /// setter + getter  
 }  

Hibernate config xml
 <?xml version="1.0" encoding="UTF-8"?>  
 <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"   
 "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">  
 <hibernate-configuration>  
   <session-factory>  
    <property name="show_sql">false</property>  
    <property name="format_sql">true</property>  
    <property name="dialect">org.hibernate.dialect.MySQLDialect</property>  
    <property name="connection.driver_class">com.mysql.jdbc.Driver</property>  
    <property name="connection.url">jdbc:mysql://localhost:3306/test</property>  
    <property name="connection.username">root</property>  
    <property name="connection.password"></property>  
    <property name="hibernate.hbm2ddl.auto">update</property>  
    <property name="hibernate.show_sql">true</property>  
    <property name="hibernate.search.default.directory_provider">filesystem</property>  
    <property name="hibernate.search.default.indexBase">/tmp/lucene/indexes</property>  
    <mapping class="com.srccodes.example.hibernate.Contact"/>  
   </session-factory>  
 </hibernate-configuration>  

untuk menentukan dimana kita menyimpan file index nya.

 <property name="hibernate.search.default.directory_provider">filesystem</property>  
 <property name="hibernate.search.default.indexBase">/tmp/lucene/indexes</property>  


Hibernate session until (kalau kita mengunakan JPA diatas hibernate kita bisa menggunakan entity manager)

 public class HibernateUtil {  
   private static SessionFactory sessionFactory = null;   
   private static ServiceRegistry serviceRegistry = null;   
   private static SessionFactory configureSessionFactory() throws HibernateException {   
     Configuration configuration = new Configuration();   
     configuration.configure();   
     Properties properties = configuration.getProperties();  
    serviceRegistry = new ServiceRegistryBuilder().applySettings(properties).buildServiceRegistry();       
     sessionFactory = configuration.buildSessionFactory(serviceRegistry);   
     return sessionFactory;   
   }  
   static {  
    configureSessionFactory();  
   }  
   private HibernateUtil() {}  
   public static Session getSession() {  
    return sessionFactory.openSession();  
   }  
 }  

Create indexing pada setiap insert, update, delete data agar index di lucene terupdate mengikuti data di tabel.

 private void doIndex() throws InterruptedException {  
   Session session = HibernateUtil.getSession();  
   FullTextSession fullTextSession = Search.getFullTextSession(session);  
   fullTextSession.createIndexer().startAndWait();  
   fullTextSession.close();  
 }  

Create fulltext session dan lucene query yang nantinya akan diterjemahkan ke JPA/hibernate query. Jadi kita tidak perlu secara detail tahu tentang lucene query karen sudah dihandle oleh si hibernate.

 private List<Contact> search(String queryString) {  
   Session session = HibernateUtil.getSession();  
   FullTextSession fullTextSession = Search.getFullTextSession(session);  
   QueryBuilder queryBuilder = fullTextSession.getSearchFactory().buildQueryBuilder().forEntity(Contact.class).get();  
   org.apache.lucene.search.Query luceneQuery = queryBuilder  
   .keyword().onFields("name").matching(queryString).createQuery();  
   org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery(luceneQuery, Contact.class);  
   List<Contact> contactList = fullTextQuery.list();  
   fullTextSession.close();  
   return contactList;  
 }  

jika dilihat di sq log maka query ditabel contact hanya mengunakan where id (yg didapat dari lucene index) tidak ada pencarian berasarkan 'name'. Karena name sudah dihandle oleh hibernate-search.

 Hibernate:   
   select  
     this_.id as id1_0_0_,  
     this_.email as email2_0_0_,  
     this_.name as name3_0_0_   
   from  
     contact this_   
   where  
     (  
       this_.id in (  
         ?  
       )  
     )  


Berikut contoh pencarian data dengan beberapa keyword :
 >>>>>>Record found for 'paolo coelho' count : 1  
 Id: 2 | Name:Paolo Coelho | Email:paolo@gmail.com  
 ===============================================  
 >>>>>>Record found for 'mr coelho paolo is his name' count : 1  
 Id: 2 | Name:Paolo Coelho | Email:paolo@gmail.com  
 ===============================================  
 >>>>>>Record found for 'who is mister coelho paolo ?' count : 1  
 Id: 2 | Name:Paolo Coelho | Email:paolo@gmail.com  
 ===============================================  
 >>>>>>Record found for 'famous writer paolo coelho' count : 1  
 Id: 2 | Name:Paolo Coelho | Email:paolo@gmail.com  
 ===============================================  

Sekedar tambahan file index lucene bisa dibuka menggunakan software Luke.


Setiap kata di property Contact -> name di index oleh lucene (dan disimpan Id nya). Dari sinilah hibernate mencari index beradasarkan keyword yang diinginkan





Happy coding & selamat menjalankan ibadah puasa ramadhan bagi yang menjalankan.