Integracja Oracle Apex z Neo4j - użycie grafowej bazy danych do wyszukania podobnych artykułów w bazie danych.

Neo4j oferuje niesamowitą prędkość odczytu informacji. Dlaczego więc nie wykorzystać tej bazy danych np. do wyszukania podobnych produktów w bazie danych. Silnik wyszukiwarki możemy zbudować w SQL ale biblioteka Lucene z której korzysta Neo4j da nam lepsze rezultaty uzyskane dużo niższym wysiłkiem. Wyszukiwarka oparta o Neo4j zwróci rządaną liczbę rezultatów oznaczonych współczynnikiem podobieństwa do wyszukiwanej frazy (scoring). Więcej o Fulltext search dowiesz się z tej strony.

Z naszych poprzednich artykułów na blogu dowiesz się jak szybko uruchomić grafową bazę danych Neo4j, jak połączyć się z nią poprzez API i jak skonfigurować index fulltext. Dzisiaj zajmiemy się połączeniem do Neo4j z Oracle Apex.

Neo4j - uruchomienie bazy z produktami

Po zainstalowaniu i uruchomieniu serwera Ne4j skonfiguruj bazę danych. Zacznij od utworzenia indeksów. Jeśli nody przechowujące produkty będą nazywać sie 'product' a unikalny numer produktu do 'product_id', komenda tworząca indeks bedzie mieć postać:

CREATE CONSTRAINT product_id ON (p:product) ASSERT p.product_id IS UNIQUE

Produkty możesz utworzyć na wiele sposobów. Ładując je poleceniem 'LOAD CSV' bazy danych, używając HTTP API lub wykorzystując komponenty Pentaho PDI (jeden z łatwiejszych sposobów). Polecenie zapewniajace sprawdzenie przed załadowaniem produktu czy nie znajduje się już podobny może mieć postać:

MERGE (p:product { product_id: 100, product_name: "opis artykułu" })

Ostatnią czynnością jest utworzenie indeksu typu fulltext na polu 'product_name'. Tworzymy go poleceniem:

CALL db.index.fulltext.createNodeIndex("ProductNameIndex",["product"],["product_name"])

Więcej o przygotowaniu bazy danych Ne4j dowiesz się z tego artykułu.

Oracle Apex - połączenie do HTTP API

Nasza procedura PL/SQL prześle do grafowej bazy Neo4j zapytanie o wyszukanie 12 podobnych opisów produktów - prześlemy opis produktu. W rezultacie Neo4j zwróci 12 maksymalnie 12 wierszy podobnych fraz + numer produktu + scoring każdego z wierszy (wyższa liczba oznacza lepszy traf).

Rezulat zostanie przesłany w formacie JSON ktory poddamy parsowaniu a następnie umieścimy rekordy w tabeli. Wcześniej, przed operacja parsowania JSON wykasujemy zawartość tabeli która tymczasowo przechowuje rezulat wyszukiwania.

DECLARE
  v_file_name  VARCHAR2 (25);
  v_json       VARCHAR2(2000);
  v_clob       CLOB;

  l_num_cols   NUMBER;
  l_num_data   NUMBER;
  l_col        VARCHAR2(80);
  l_var        VARCHAR2(80);
 
  l_sql        VARCHAR2(200);
  l_desc       VARCHAR2(200);
  l_score      VARCHAR2(50);
  l_product_id NUMBER(8);
 
  v_test varchar(400);
  v_test_num number(8);

  v_article_description VARCHAR2(80) := :P51_ARTICLE_DESCRIPTION; -- your article desc field item
  v_hostname   VARCHAR2(30) := 'localhost'; -- your hostname, eg: localhost
  v_port       NUMBER       :=  7474; -- port for your Ne04j server, eg: 7474
  v_username   VARCHAR2(50) := 'neo4j'; --:Neo4j server username
  v_password   VARCHAR2(50) := 'some_password'; -- :Neo4j server password
 
  v_authorization_string VARCHAR2(30) := v_username || ':' || v_password;
  v_rest_url VARCHAR2(300) := 'http://' || v_authorization_string || '@' || v_hostname || ':' || v_port || '/db/neo4j/tx/commit';

BEGIN
   --delete old content from table
    l_sql := 'DELETE FROM similar';
    execute immediate l_sql;
    commit;
    l_sql := '';

   -- prepare JSON:
   v_json := '{"statements" : [ {"statement" : "CALL db.index.fulltext.queryNodes(\"ProductNameIndex\", \"product_name: '|| v_article_description  ||'\") YIELD node, score RETURN node.product_name, node.product_id AS product_id, score LIMIT 12"} ]}';
 
   apex_web_service.g_request_headers(1).name := 'Content-Type';  
   apex_web_service.g_request_headers(1).value := 'application/json';
 
  -- POST JSON to server
  v_clob := apex_web_service.make_rest_request(
    p_url => v_rest_url,
    p_http_method => 'POST',
    p_body => v_json
  );
 
  apex_json.parse(v_clob);
  l_num_cols := apex_json.get_count('results[1].columns');
  l_num_data := apex_json.get_count('results[1].data');
 
    FOR l_idx IN 1..l_num_data LOOP
    -- each column in rows[]
     FOR l_idx_cols IN 1..l_num_cols LOOP
        l_col := apex_json.get_varchar2('results[1].columns[%d]', l_idx_cols);
        l_var := apex_json.get_varchar2('results[1].data[%d].row[%d]', l_idx, l_idx_cols);
        
        --description column:
        if l_col = 'node.product_name' then l_desc :=  l_var;
        end if;
        --product_id
        if l_col = 'product_id' then l_product_id :=  l_var;   
        end if;
        --score column:
        if l_col = 'score' then l_score :=  TO_NUMBER(l_var);   
        end if;

    END LOOP;
    
          l_sql := 'INSERT INTO similar (product_id, product_name, score) VALUES (' || l_product_id || ',''' || l_desc || ''', ''' || ROUND(l_score, 4) ||''')';
          execute immediate l_sql;
          commit;
    
    END LOOP;

 EXCEPTION
  WHEN OTHERS THEN
    RAISE;
 
END;

Wyszukanie 12 podobnych produktów w bazie danych Neo4j w której znajduje się 50 tysięcy produktów zajmuje mniej niż sekundę. Zdecydowanie dłużej parsuje i wczytuje się w przeglądarce strona Oracle Apex. Więcej o HTTP API Neo4j przeczytasz tutaj.