Wyszukiwanie węzłów w Alfresco, Część 2

25 lipca 2009 | 0 komentarze

Przykłady zapytań - wyszukiwanie dokumentów

Znajdź dokumenty utworzone przez "a.głowacki"
TYPE:"cm:content" AND PATH:"/app:company_home//*" AND @cm\:creator:a.głowacki
Dokumenty utworzone lub modyfikowane 15 lipca 2009
TYPE:"cm:content" AND PATH:"/app:company_home//*" AND (@cm\:created:2009-07-15* OR @cm\:modified:2009-07-15*)
Wyszukanie wszystkich wersji dokumentu "crystal.txt" (store workspace://version2Store)
TYPE:"cm:content" AND @cm\:name:crystal.txt

Przykłady - kategorie


Znajdź kategorię "Dokumentacja Techniczna" bez względu na położenie w hierarchii
@cm\:name:"Dokumentacja Techniczna" AND TYPE:"cm:category"
Ta sama kategoria ale po ścieżce (niebezpieczne - zmiana nazwy nie zmieni QName)
PATH:"/cm:generalclassifiable//cm:Dokumentacja_x0020_Techniczna"

Przykład WebScripts


Skrypty WebScripts to sposób na szybką i bezinwazyjną integrację z Alfresco. Plusy: bogate API, architektura REST, mnóstwo przykładów, transakcyjność. Minusy: mix JavaScript + FreeMarker = jak to debugować???

Skrypt znajduje w repozytorium dokument o podanej nazwie (audyt.doc) i przypisuje do niego konkretną kategorię (Raporty). Widok wyświetla renerencję do dokumentu. Url skryptu: http://localhost:8080/alfresco/service/searchexample

searchexample.get.js:
go();

function go() {

var fileName = "audyt.doc";
var categoryName = "Raporty";
var docNode = findDocument(fileName);
var categoryNode = findCategory(categoryName);

if (docNode == null)
return showError(400, "Document not found: "+fileName+".");

if (categoryNode == null)
return showError(400, "Category not found: "+categoryName+".");

if(!docNode.hasAspect("cm:generalclassifiable")) /* pozwol przypisac kategorie */
docNode.addAspect("cm:generalclassifiable");

if(docNode.properties["cm:categories"] == null) /* just in case */
docNode.properties["cm:categories"] = new Array();

docNode.properties["cm:categories"].push(categoryNode);
docNode.save();
model.resultset = docNode; /* kontekst widoku */
}

function findDocument(name){
var result = search.luceneSearch("TYPE:\"cm:content\" AND PATH:\"/app:company_home//*\" AND @cm\\:name:\""+name+"\"");
if(result.length>0)
return result[0];
return null;
}

function findCategory(name){
var result = search.luceneSearch("TYPE:\"cm:category\" AND @cm\\:name:\""+name+"\"");
if(result.length>0)
return result[0];
return null;
}

function showError(errorCode, msg){
status.redirect = true;
status.code = errorCode,
status.message = msg;
}
Deskryptor (searchexample.get.desc.xml):
<webscript>
<shortname>Wyszukiwanie Lucene</shortname>
<description>
Pulished on publicmethods.blogspot.com
</description>
<url>/searchexample</url>
<format default="text">argument</format>
<authentication>user</authentication>
<transaction>required</transaction>
</webscript>
Szablon widoku (searchexample.get.text.ftl):
${resultset.nodeRef.storeRef.protocol}/${resultset.nodeRef.storeRef.identifier}/${resultset.nodeRef.id}/${resultset.name?url}


Linki
http://lucene.apache.org/java/2_4_1/queryparsersyntax.html
http://wiki.alfresco.com/wiki/Search_Documentation
http://wiki.alfresco.com/wiki/Full_Text_Search_Query_Syntax
http://wiki.alfresco.com/wiki/Alfresco_Namespaces
http://wiki.alfresco.com/wiki/JavaScript_API

Wyszukiwanie węzłów w Alfresco, Część 1

20 lipca 2009 | 0 komentarze

Alfresco wspiera wyszukiwanie węzłów (nodes) przez zapytania XPath i Lucene. Implementacja pierwszego jest wymagana przez specyfikację JSR-170. Lucene daje za to większe możliwości rozbudowy języka zapytań i jest metodą rekomendowaną.

Query można uruchamiać wprost z GUI - z poziomu Node Browser (link w konsoli administracyjnej). Po uruchomieniu Browser prosi o wybranie workspace (workspace://SpacesStore).


Alfresco udostępnia API do wyszukiwania: SearchService (JAVA) i Search API (WebScripts).

Poniżej zamieszczam przegląd możliwości języka Lucene z przykładami. Wyniki zapytań mogą różnić się w zależności od wersji.

Wyszukiwanie po dowolnych polach: (@xx/:nazwa:)

Nazwy pól są w postaci QName (qualified name) - występują w formie pełnej (pełna nazwa namespace z protokołem) lub skróconej.
@cm\:userName:"admin"
@cm\:lastName:"w*"
Wyszukiwanie pełnotekstowe (TEXT)
TEXT:"izotopy węgla"
Wyszukiwanie po referencji (ID), referencji rodzica (PARENT , PRIMARYPARENT)

Referencja węzła (NodeRef) = protokół://workspace/UUID obiektu
ID:"workspace://SpacesStore/510e0049-7d3f-4f3e-823d-8314b749b4a9"
PARENT:"workspace://SpacesStore/984bffc5-ef6c-4e72-9fc6-7d353d7e5264"
Ścieżka XPath (PATH)

Ścieżka nie może zawierać znaków specjalnych - wymagane kodowanie ISO9075.
PATH:"/app:company_home"
PATH:"/app:company_home/app:guest_home"
PATH:"/app:company_home/app:user_homes/*"
PATH:"/app:company_home//*"
Według przypisanego aspektu (ASPECT, EXACTASPECT)
TYPE:"cm:content" AND ASPECT:"cm:versionable"
Typ węzła (TYPE)

Podstawowe typy węzłów: cm:content, cm:link, cm:category, cm:person, cm:folder.
TYPE:"cm:category"
Nazwa kwalifikowana (QNAME)

QName pod którym jest widoczny węzeł, bez namespace, kodowanie ISO9075.
QNAME:faktura_x0020_42812.ods
QNAME:f*
QNAME:*.ods
Stan pola (ISUNSET, ISNULL, ISNOTNULL)
ISNULL:"cm:title"
Operatory (+, -, AND, OR, NOT), wildcards (*, ?), grupowanie
TYPE:"cm:content" AND TEXT:"L?n*" - ASPECT:"cm:versionable"
(TYPE:"cm:category" AND TEXT:"k*") OR (TYPE:"cm:folder" AND QNAME:k*)
Zakresy wartości: zakmnięty [... TO ...], otwarty {... TO ...}
@cm\:lastName:[c TO e]
@cm\:lastName:{talar TO toczko}
Wyszukiwanie podobnych fraz - odległość Levenshtein'a

Parametr określa stopień podobieństwa w zakresie od 0 (małe) do 1 (duże) - domyślnie 0.5.
TEXT:alkan~
TEXT:hiduminium~0.3
Bliskość położenia frazy (proximity)

Parametr wskazuje maksymalną odległość słów od siebie.
TEXT:"endopeptydaza pepsyna"~7
Priorytet frazy (boost)

Liczba wskazuje ważność.
TEXT:"Notogea" OR TEXT:"Antarktis"^2

Więcej na stronach wiki.

JackRabbit code review '05

16 lipca 2009 | 0 komentarze

Przeglądając wiki Alfresco natrafiłem na archiwalny raport z audytu kodu Apache JackRabbit'a. Opis dotyczy wersji 0.16.2 z okolic 2005 roku - wtedy w fazie inkubacji. Panowie zastanawiają się nad użyciem implementacji Apache w architekturze powstającego ECM Alfresco. Poniżej ciekawsze fragmenty.

Andy (Hind ?):
  • No version control
  • Persistence is not synchronised and will have threading issues
  • Not sure if they have workspaces> (There is no system workspace and noy dynamic workspace. I think there is just one * workspace) - Could support this in subversion style
  • There can be multiple instantiations pointing at the same repository which will cause havoc.
  • Does authentication and authorization work – looks very simple …

David (Caruana ?):
In the bug tracking system, the following comment may back this up - "i must admit that i took no care about multihtreading until now. but of course i will do it now." - a comment from the guy who wrote the versioning piece.

When you look at the code, you get the Slide v1.0 feeling. There are going to be bugs due to the amount of in-memory data structures that need to be kept just right - in a multi-threaded context. It's not the sort of thing that's easily fixed - there will be a Slide 3.0 equivalent when a re-design of the architecture is considered.

Given all this caching, hierarchy related processing performance will still suffer. There's no indexing as far as I can see.

As Andy has pointed out, there are numerous holes and inconsistencies - I expect these will get fixed as time goes by.

What do we do with JackRabbit?

We could:
1. Use it as our basic Repository capability - build our stuff around it
2. Build our own Repository capability and use JackRabbit where possible to provide a JSR-170 facade
3. Not use it at all

Option 1 might be viable if it's a stepping stone to building a community quickly with the intention of replacing it.

But, my preference is option 2. We'd probably gut JackRabbit anyway (not very nice!!) to cater for native db implementation and subversion type capability and also to fix reliability issues. However, it would give a head-start to providing a facade to our own stuff e.g. Type Model, Query etc.


Pełna wersja raportu.

Monitorowanie Tomcat'a i Lambda Probe

10 lipca 2009 | 0 komentarze

Dzisiaj swoją szanse dostała Lambda Probe (wcześniej Tomcat Probe) - aplikacja do administracji i monitorowania serwera Apache Tomcat. Instalacja w formie war'a, licencja GNU GPL, ładne GUI - to atuty znane przed instalacją ze strony produktu.

Najważniejsze ficzery

-działa na Tomcie od 5.0 wzwyż i na jBossie
-na dzień dobry wszystko to co daje Manager
-szczegółowe monitorowanie pamięci + wykresy
-monitorowanie obciążenia CPU
-szczegółowe monitorowanie wątków, możliwość zabicia wątku
-monitoriwanie ruchu i liczby zapytań w podziale na connectors
-przeglądanie plików JSP, re-kompilacja JSP
-lista i przeglądanie logów (Tomcat 6 => tylko LOG4J)
-lista DataSource, test połączenia, wykonanie zapytania SQL
-monitorowanie klastra + wykresy ruchu
-możliwość restartu JVM - wymagana istalacja Java Service Wrapper

Pełna lista możliwości tutaj.


Instalacja

Jest bardzo prosta. Z oficjalnej strony pobieram binarkę. Dla własnego komfortu publikuję wara w formie exploded. W środku JSP, Spring i SiteMesh - żadnych szaleństw i oto chodzi. Na JVM na której pracuje serwer konieczne jest włączenie JMX agent'a z dostępem lokalnym przez dodanie parametru:
-Dcom.sun.management.jmxremote
Po restarcie serwera aplikacja wstaje za pierwszym razem. Autentykacja przez kontener, wymagany user posiadający rolę "manager" - tak jak w Tomcat Manager (do wyboru są jeszcze role probeuser, poweruser, poweruserplus). Instalacja poszła gładko.

Pierwsze wrażenie

Bardzo ładny, czytelny, funkcjonalny interfejs. Od razu wiadomo gdzie czego szukać. Automatyczne odświeżanie wykresów i tabel w Ajax'ie (prototype + scriptaculous). Jest kilka przydatnych bajerów - każdy wykres jednym kliknięciem można powiększyć na pełny ekran. W porównaniu do Tomcat Manager'a to skok w inną epokę.


Drugie wrażenie

Nie wszystko działa idealnie. Irytuje wyskalowanie wykresów do maksymalnej aktualnie wyświetlanej wartości, zamiast do maksymalnej możliwej. Górną granicą Perm Gen powinien być MaxPermSize! Dobrze że w tabelce jest procentowy progres.

Brakuje powiadomień. Chciałbym dostać e-maila z informacją jeśli dzieje się coś niedobrego. Nie ma możliwości zachowania pełnych danych historycznych, nie mówiąc o wysłaniu ich w mailu. Domyślnie wykresy przedstawiają tylko ostatnie 2 godziny. Na szczęście można poprosić Spring'a o ustawienie innych niż domyślne triggerów Quartz (30 sekund) i zmienić długość przechowywanych serii (/WEB-INF/spring-stats.xml). Trzeba uważać żeby nie przesadzić - dane są serializowane przez XStream na bieżąco do pliku (/work/Catalina/localhost/probe/stats.xml) i przy większym rozmiarze serii może wystąpić problem wydajnościowy. Poniżej przykład zmiany długości strumienia do 720 - przy logowaniu co 30sek. daje to 6 godzin (spring-stats.xml).

<bean name="memoryStatsCollector"
class="org.jstripe.tomcat.probe.beans.stats.collectors.JvmMemoryStatsCollectorBean">
<property name="jvmMemoryInfoAccessor" ref="jvmMemoryInfoAccessor"/>
<property name="statsCollection" ref="statsCollection"/>
<property name="maxSeries" value="720"/>
</bean>


Martwi brak aktualizacji projektu przez ostatnie 3 lata - najnowsza wersja 1.7b (beta?) jest z listopada 2006. Pewnie dlatego że jest taka dobra i nie zawiera żadnych błędów. ;)

Lambda to bardzo dobre narzędzie. Praca z takim GUI to czysta przyjemność. Od dzisiaj pierwszym krokiem po instalacji Tomcata powinno być wyrzucenie Managera i instalacja w to miejsce Lambda Probe. ;)

Indeksowanie pól typu data/czas w Apache Lucene

8 lipca 2009 | 0 komentarze

Zadanie: wykonanie wyszukiwania w Lucene artykułów w określonym przedziale czasowym i wyświetlenie daty publikacji bez czytania z bazy danych. Indeksowanie pola typu data.

Ekipa Lucene zaleca zapisywanie daty jako zwykłe pole tekstowe po konwersji do typu String (klasa dedykowana do czasu DateField jest oznaczona jako przestarzała @Deprecated). Najbardziej wydajne jest indeksowanie minimalnej potrzebnej dokładności czasu - im wieksza precyzja, tym dłuższy String i większe obciążenie wydajnościowe podczas wyszukiwania. Data jest konwertowana do formatu który pozwala na sortowanie leksykalne, np. dla dokładności rzędu pojedynczego dnia: CCYYMMDD (20090503 = 3 maja 2009). Dodatkowym plusem takiego formatu ma być możliwość zapisania daty sprzed 1970 roku.

Do zamiany czasu na tekst służy klasa org.apache.lucene.document.DateTools której metody statyczne wykonają robotę w obie strony. Do wyboru są metody przyjmujące jako argument typ java.util.Date i long (milisekundy). Statyczna klasa zagnieżdżona DateTools.Resolution pozwala na określenie dokładności konwersji (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, MILLISECOND).

static String dateToString(Date date, DateTools.Resolution resolution)
static String timeToString(long time, DateTools.Resolution resolution)
static Date stringToDate(String dateString)
static long stringToTime(String dateString)


Przykład indeksowania daty z dokładnością do dnia:

Directory directory = getDirectory();
IndexWriter writer = getWriter(directory);
Document doc = new Document();
doc.add(new Field(”body”, text, Field.Store.YES, Field.Index.ANALYZED));
String dateStr = null;
if(content.getDate()!=null)
dateStr = DateTools.dateToString(content.getDate(), DateTools.Resolution.DAY);
doc.add(new Field(”date”, dateStr, Field.Store.YES, Field.Index.NOT_ANALYZED));



Przykład kodu wyszukującego po przedziale czasowym i odczytanie daty z indeksu:

Directory directory = getDirectory();
IndexSearcher searcher = new IndexSearcher(directory);
RangeQuery rquery = new RangeQuery(new Term("date", "20090401"), new Term("date", "20090601"), true);
TopDocs docs = searcher.search(rquery, 100);
ScoreDoc[] scoreDocs = docs.scoreDocs;
for(int d=0;d
ScoreDoc scoreDoc = scoreDocs[d];
Document doc = searcher.doc(scoreDoc.doc);

String body = doc.get(”body”);
String dateStr = doc.get(”date”);
Date date = null;
try {
date = DateTools.stringToDate(dateStr);
} catch (Exception e) { }
System.out.println(”body:”+body);
System.out.println(”data:”+date);
}
searcher.close();


JavaDoc zaleca użycie ConstantScoreRangeQuery w miejsce RangeQuery, bo ponoć jest szybsze i bezpieczniejsze (nie wyrzuca wyjątku BooleanQuery.TooManyClauses przy przektoczeniu 1024 wyników). W miejsce Query można rozważyć zastrosowanie filtra RangeFilter.


Linki:
http://wiki.apache.org/lucene-java/DateRangeQueries
niektóre wpisy w wiki Lucene są nieaktualne