[Uyarı: Bu uzun dökümandan en fazla yarar sağlamak için rahat bir zamanda okuyunuz. Dökümanın tamamı Tayfun Şen tarafından yazılmıştır. ]


Bu dökümanda PostgreSQL veritabanı üzerinde Türkçe tam metin arama özelliklerinin nasıl kullanılabileceğini anlatacağım. Önce sürüm meselesini bir aradan çıkaralım. Tam metin arama özellikleri son PostgreSQL sürümleri ile birlikte oldukça iyileştirildi. Bundan dolayı ben şu anda son kararlı sürüm olan 8.3.6 sürümünü kullanacağım. İşletim sistemi olarak tercihim Linux, ve dağıtım olarak da Debian öneriyorum. Tabi herhangi bir Linux dağıtımı işinizi görecektir. Eğer zavallı bir Vindoz kullanıcısı iseniz anlatacaklarımı biraz değiştirmeniz gerekebilir (siz en iyisi Linux'a geçin :)).

Eğer yeni bir kurulum yaptıysanız root kullanıcısı olun ve eğer veritabanı sunucusu çalışmıyorsa onu başlatın. Bunun için, örneğin Debian'da,

peanutbutter:/home/tayfun# /etc/init.d/postgresql-8.3 start
Starting PostgreSQL 8.3 database server: main.

demeniz yeterli.

Bundan sonra

peanutbutter:/home/tayfun# su postgres

ile postgres kullanıcısına geçiş yapın. postgres kullanıcısı veritabanı üzerinde tam yetkiye sahip bir yönetici kullanıcısıdır. Bu yönetici kullanıcısı olduktan sonra psql ile veritabanına bağlanabilirsiniz:

postgres@peanutbutter:/home/tayfun$ psql
Welcome to psql 8.3.6, the PostgreSQL interactive terminal.

Type: \copyright for distribution terms
\h for help with SQL commands
\? for help with psql commands
\g or terminate with semicolon to execute query
\q to quit

Veritabanı yaratmadan önce localization ve i18n hakkında bahsetsem iyi olacak. Eğer Türkçe veri saklayacaksanız veritabanınızın Türkçe karakter kodlamasını kullanması gerekiyor. PostgreSQL, MySQL kadar esnek bir karakter kodlama özelliklerine sahip değil. Bu yüzden her veritabanı kümesi (cluster) için
başlangıçta (initdb ile) verilan LC_COLLATE ve LC_CTYPE gibi önemli yerelleştirme değerleri daha sonra değiştirilemez. PostgreSQL varsayılan olarak
sistem locale değerini kullanıyor. Benimki UTF-8 olduğu için veritabanım da bu kodlamayı kullanıyor. Debian'da locale değerini 'locale' komutu ile öğrenebilirsiniz:

tayfun@peanutbutter:~$ locale
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=


Şimdi bir veritabanı yaratalım ve bağlanalım:

postgres=# create database deneme;
postgres=# \c deneme

Burada durup PostgreSQL'in bize tam metin arama için sağladığı güzelliklere değinelim. Eğer PostgreSQL dökümanlarını okursanız tsvector ve tsquery gibi iki veri yapısını sıklıkla duyacaksınız. Bunlardan tsvector dökümanınızı arama yapmaya uygun halde tutar, tsquery ise arama yaparken kullandığınız sorgular için tasarlanmıştır.

Şimdi tam metin için örnek bir veritabanı tablosu oluşturalım. Bu tabloya ntvmsnbc'deki bazı haber metinlerini ekleyeceğim. Tablomuzda tipik olarak başlık, özet, anahtar kelimeler, tam metin, yazar gibi alanlar olacak.

deneme=# create table metin (baslik varchar(100), ozet text, anahtar_kelimeler varchar(200), tam_yazi text, yazar varchar(200));

Şimdi biraz veri yükleyelim:

deneme=# insert into metin values ('İhracat yüzde 35 geriledi', 'İhracat Şubat ayında yüzde 35 düşerek 6.9 milyar dolar olarak gerçekleşti. Otomotiv ihracatında yüzde 55 azalma yaşandı.', 'ihracat, otomotiv, gerileme, kriz', 'Krizin etkisiyle pazarlardaki daralmayla ihracatta kan kaybı devam ediyor. Türkiye İhracatçılar Meclisi (TİM) verilerine göre Şubat ayında ihracat önceki yılın aynı dönemine göre yüzde 35 düşüşle 6.9 milyar dolar oldu. Ocak-Şubat döneminde ihracat yüzde 31''lik azalışla 13.9 milyar dolar olarak gerçekleşti. Son 12 aylık ihracat verilerinde ise yüzde 8.1 artış yaşandı. Böylece toplam yıllık ihracat 121.7 milyar dolar oldu. Türkiye İhracatçılar Meclisi (TİM) Başkanı Mehmet Büyükekşi, Şubat ayı ihracat rakamlarını açıklamak için Uludağ İhracatçı Birlikleri''nde düzenlediği basın toplantısında, otomotiv sektörünün geçen ay 1 milyar 98 milyon dolarlık ihracat gerçekleştirerek, dış satımda sektörler arasında liderliğini sürdürdüğünü ve 1 milyar dolar ihracat yapan tek sektör olduğunu belirtti. Otomotivi 984 milyon dolar ile demir - çelik ve 967 milyon dolar ile hazır giyim ve konfeksiyon sektörlerinin takip ettiğini anlatan Büyükekşi, tarım ve hayvancılık grubu sektörlerinin ihracatının ise Şubat ayında yüzde 3.31 gerilerken, toplam ihracat içinde yüzde 14.39''luk pay aldığını bildirdi.', 'Ajanslar');


deneme=# insert into metin values ('Şahin: Sözlerim maksadını aştı', 'Adalet Bakanı Şahin, "Hükümetle zıtlaşan yerel yönetimler her projelerini Ankara’dan geçiremiyor" sözleri için "Yanlış ve maksadını aşan bir ifadedir" dedi.', 'şahin, adalet bakanı, şantaj, rant', 'TBMM''de konuşan muhalefet milletvekilleri Adalet Bakanı Mehmet Ali Şahin''in hafta sonu Antalya''da söylediği "Hükümetimizle kavga eden, zıtlaşan yerel yönetimler her projelerini Ankara’dan geçiremiyor. Maalesef bu Türkiye’nin gerçeği" sözlerini eleştirdi. Kürsüye gelen Adalet Bakanı Şahin, eleştirileri anlayışla karşıladığını belirtti. Adalet Bakanı Şahin, şöyle konuştu: "Orada yaptığım konuşmalarda genel olarak vermek istediğim mesaj şuydu; bir yerel yönetimin, bir belediyenin, bir belediye başkanın, başka belediyelerin veya merkezi yönetimin yardımı olmadan çözemeyeceği bir takım sorunları vardır. O nedenle belediye başkanı olarak seçeceğiniz arkadaşın, diğer belediyelerle, diğer kuruluşlarla diyaloğu devam ettirecek, onların da yardımını alabilecek, onlarla birlikte çalışabilecek kapasitede arkadaşlar olması gerektiğini ifade ettim. Bunu söylerken, şu partiden, bu partiden ayrımı yapmadım. Ama Gazipaşa''da ''Eğer seçeceğiniz belediye başkanı bizim partiden olursa memnun olurum'' anlamına gelen bir ifade kullandım. Yerel yöneticilerin büyükşehir belediyesi ve merkezi hükümetle de uyum halinde, diyalog halinde olmasının yararlı olacağını düşündüm. Beni buraya getiren neydi? Ben Antalya milletvekiliyim ve kabinede görev yapıyorum. Antalya''da bazı ilçe ve beldeleri ziyaret ediyorum. Mesela bazı ilçeler var ki, henüz altyapıları, kanalizasyon sorunları bile büyük ölçüde çözülmedi. Ben bizim partiden olmayan bu belediyelerin bulunduğu yerlere gittiğimde mevcut belediye başkanı arkadaşlarımla bir diyalog kurayım, acaba birlikte burada bir çözüm üretebilir miyiz'' dediğimde, bazı ilçeler var ki bu belediye başkanlarıyla ben maalesef 3-4 yıldır tanışma şerefine nail olamadım. Ben bunu bazı yerlerde eleştirdim. ''Keşke bu belediye başkanı arkadaşlarımla diyalog kurabilsek, başka partiden olabilir ama belki buradaki sorunlara birlikte çözüm üretebiliriz'' diye bir takım konuşmalar da yaptım."', 'ajanslar');


deneme=# insert into metin values ('Gül dava açtı, Arıtman ısrarlı', 'Cumhurbaşkanı Gül ve CHP’li Arıtman arasındaki tartışma yeni bir boyut kazandı. Cumhurbaşkanı tazminat davası açtı, Arıtman ısrarlı açıklamalarını sürdürdü: Nüfus kütükleri Türk olduğunu kanıtlayamaz.', 'canan arıtman, gül, saçmalıklar, politikacıların zeka seviyesi', 'Cumhurbaşkanı Gül, “Hakkımda bir yalan yaymak istiyorlar” iddiasıyla, kendisi için “Annesinin kökenine bir bakın” diyen CHP milletvekili Canan Arıtman hakkında 1 YTL''lik manevi tazminat davası açtı. Gül''ün etnik kökenleri konusunda açıklama yapmasına sebep olan iddiaları ileri süren Arıtman ise, bugün de “Nüfus kütüklerinden köken belli olmaz” diyerek ısrarlı tavrını sürdürdü. CHP İzmir Milletvekili Canan Arıtman, 1915 olayları için başlatılan “Özür diliyorum” kampanyasına tepki göstermediği gerekçesiyle Cumhurbaşkanı Abdullah Gül''ü eleştirmiş, “Tabii ki destekler, annesinin kökenine bir bakın” demişti.

Arıtman''ın bu açıklamasından sonra önce sessiz kalan Cumhurbaşkanı Gül, pazar günü yazılı açıklama yaparak kendisi hakkında yayılmak istenen bir yalanı düzeltmek istediğini belirtti. Gül, açıklamasında “Kayseri’nin yerlisi olan annem tarafından Satoğlu, babam tarafından Gül sülalelerinden gelen ailemizin yüzyıllara uzanan kayıtlı geçmişi Müslüman ve Türk''tür. Buna ailemizin geçmişten günümüze birlikte titizlikle işlenen soy ağacımız, mevcut resmi nüfus kütükleri ve gelmiş geçmiş Kayseri''li hemşehrilerimiz şahittir” ifadesini kullanmıştı.', 'NTV-MSNBC');

Şimdi tablomuzda üç tane satır var. Verilerimizi de eklediğimize göre bu tabloda tam metin arama nasıl yapabiliriz? Aslında çok zor birşey değil. Örnek tablomuzdaki tam yazı alanınında sorgumuzu şöyle gerçekleştirebiliriz:

deneme=# select baslik from metin where to_tsvector(tam_yazi) @@ to_tsquery('söz');
baslik
--------------------------------
Şahin: Sözlerim maksadını aştı
(1 row)

deneme=# select baslik from metin where to_tsvector(tam_yazi) @@ to_tsquery('kütük');
baslik
--------------------------------
Gül dava açtı, Arıtman ısrarlı
(1 row)

Bu iki sorguda da tam_yazi alanı to_tsvector fonksiyonu yardımı ile tsvector haline ve sorgu için kullanacağımız kelimeler ('söz' ve 'kütük') to_tsquery fonksiyonu ile tsquery haline getiriliyor. Bundan sonra @@ operatörü ile sorgumuzu çalıştırabiliyoruz. Sorgularımızı gerçekleştirirken boolean operatörleri kullanabiliriz, &, | ve ! sembolleri yardımı ile. Örnek bir sorgu şöyle olabilir:

deneme=# select baslik from metin where to_tsvector(tam_yazi) @@ to_tsquery('türkiye');
baslik
--------------------------------
İhracat yüzde 35 geriledi
Şahin: Sözlerim maksadını aştı
(2 rows)

ama

deneme=# select baslik from metin where to_tsvector(tam_yazi) @@ to_tsquery('türkiye & çözüm');
baslik
--------------------------------
Şahin: Sözlerim maksadını aştı
(1 row)

ts_vector ve ts_query veri yapılarının nasıl tutulduğunu görmek ister misiniz? Bunu şu şekilde görebilirsiniz:

# select to_tsvector(tam_yazi) from metin where baslik='Şahin: Sözlerim maksadını aştı';

'3':211 'e':19 'p':105,219 'u':138 '-4':212 'be':146 'et':104 'he':170 'in':11 'mi':200 'ne':149 'on':92,96 'so':13 'ali':9 'ayr':111 'ben':150,178,209,218 'bil':174 'bir':58,61,63,74,127,191,197,241 'biz':120,179 'dan':26 'haf':12 'hal':1
...
...
...

Bu SQL sorgusunun sonucu olarak köklerine ayrılmış kelimeleri ve her birinin dökümandaki yerini göreceksiniz. Örnek olarak 'bir' kelimesi 58, 61, 63. sırada görülmüş (ve daha birçok yerde).

Bunu daha basit şekilde de deneyebiliriz:

deneme=# select to_tsvector('deneme cümlemiz bu olsun. olsun!');
to_tsvector
---------------------------
'ol':4,5 'dene':1 'cümle':2
(1 row)

Burada da görüldüğü gibi 'deneme' ve 'olsun' kelimelerimiz köklerine ayrıldı.

ts_query yapısını da benzer bir şekilde deneyebiliriz.

Dikkatli okuyucular az önce tabloya karşı yaptığımız sorgularda tam yazı alanlarının canlı olarak tsvector'e çevrildiğini farketmişlerdir. tsvector'ün yapısı incelendiğinde bunun maliyetli bir işlem olduğu görülebilir. Peki bunu nasıl daha kolay ve verimli bir hale sokabiliriz?

Bunun aslında iki yolu var. İlk yöntem ile sadece bir index tanımlaması yaparak aramalarımızı hızlandırabiliriz. Bunun için index'imizi to_tsvector(tam_yazi) olarak tanımlamamız gerekiyor. İkinci yöntemde ise tabloya bir alan ekleyip o alana tsvector'leri doldurabiliriz. Bu yöntemleri biraz daha detaylı inceleyelim.

İkinci yöntemimizden başlarsak, fazladan bir alan ile tsvector yapılarımızı saklamak maliyeti yüksek bir yöntem. Bunun nedeni tablonuzu büyütmesi ve bu alanın güncel tutulması için gerekecek trigger benzeri yapılarının verimlilik sorunları. Yine de bu yöntemi uygulamak istersek şu şekilde yapabiliriz:

Öncelikle trigger yazmamız gerekiyor. Bir dosya yaratın ve içine şunları yazın:

CREATE OR REPLACE FUNCTION norm_text (
text_to_normalize text
) RETURNS text AS $$
DECLARE
normalized_string text;
BEGIN
select translate(text_to_normalize, 'çğıöşüÇĞÖŞÜİ', 'cgiosuCGOSUI')
into normalized_string;

return normalized_string;

END;
$$ LANGUAGE plpgsql IMMUTABLE;


CREATE OR REPLACE FUNCTION lexeme_trigger ()
RETURNS trigger AS $$
begin
new.lexemes :=
setweight(to_tsvector('turkish', coalesce(norm_text(new.baslik), '')), 'A'
) ||
setweight(to_tsvector('turkish', coalesce(norm_text(new.anahtar_kelimeler)
, '')), 'B') ||
setweight(to_tsvector('turkish', coalesce(norm_text(new.ozet), '')), 'C')
||
setweight(to_tsvector('turkish', coalesce(norm_text(new.tam_yazi), '')), '
D');
return new;
end
$$ LANGUAGE plpgsql;

(İleride değinilecek ama burada bahsetmekte fayda var: turkish ile köklere ayırma yapılacaktır, eger yanlış işler yapıyorsa bu ayırma işlemi, sözlük zinciri yaratılabilir).

Burada tek uyguladığımız sadece bir trigger değil, tam metin aramanın başka bir özelliğinden de yararlanıyoruz. Bu özellik ağırlıklandırma. setweight fonksiyonu ile tablodaki alanlara ağırlıklar veriyoruz. A harfinden D'ye doğru ağırlık azalıyor. En önemli alan olan başlık bilgisi en fazla ağırlığa sahip, bu sayede sorgunun birden fazla satırda eşleniği varsa eşlenen alanların ağırlığına göre bir sıralama yapılacak.

Bunun dışında ilk tanımlanan fonksiyon norm_text, adından da anlaşılacağı gibi yazı normalisation yapıyor. PostgreSQL'de MySQL gibi gelişmiş collation seçenekleri yok, accent insensitive eşleme yapabilmek için kendi fonksiyonumuzu yazmamız gerekiyor. norm_text bunu yapıyor, Türkçe karakterleri değiştiriyor. Bu sayede kırmızı aradığımızda kirmizi'lari da bulacağız. Eğer bu türden bir özelliğe ihtiyacımız yoksa bu fonksiyonu ve kullanıldığı yerleri kaldırabiliriz. Accent insensitive arama yapmak istiyorsak hem tsvector oluştururken, hem de tsquery oluştururken bu fonksiyonu kullanmak durumundayız.

Bu trigger ve normalisation fonksiyonunun tanımının yapıldığı dosyayı PostgreSQL'e vermemiz gerekiyor. Ama öncesinde bu trigger'ın yazıldığı dilin (plpgsql) etkin olması gerek:

deneme=# create LANGUAGE plpgsql;

Şimdi fonksiyonumuzun bulunduğu dosyayı verebiliriz:

deneme=> \i ~tayfun/yonca/norm_text.sql
CREATE FUNCTION

Artık tsvector'lerin tutulacağı alanı oluşturabilir

deneme=> alter table metin add column lexemes tsvector;

ve trigger'ı yaratabiliriz:

deneme=> CREATE TRIGGER lexemeupdate BEFORE INSERT OR UPDATE ON metin FOR EACH ROW EXECUTE PROCEDURE lexeme_trigger();

Artık her metin eklemesinde lexeme kolonu tsvector ile güncellenecektir. Bundan sonra bu alanı kullanarak tam metin arama yapabiliriz. Eski satırlarımızı güncellemek için:

deneme=# update metin set lexemes =
deneme-# setweight(to_tsvector('turkish', coalesce(norm_text(baslik), '')), 'A') ||
deneme-# setweight(to_tsvector('turkish', coalesce(norm_text(anahtar_kelimeler), '')), 'B') ||
deneme-# setweight(to_tsvector('turkish', coalesce(norm_text(ozet), '')), 'C') ||
deneme-# setweight(to_tsvector('turkish', coalesce(norm_text(tam_yazi), '')), 'D');
UPDATE 3

Eski veri de arama yapmaya uygun hale getirildiğine göre, sorgulara başlayabiliriz:

deneme=# select baslik from metin where lexemes @@ to_tsquery('sah');
baslik
--------------------------------
Şahin: Sözlerim maksadını aştı
(1 row)

Eğer veritabanındaki lexemes alanı incelenirse kelimelerin köklerine ayrıştırılmış oldukları görünür. Bundan başka kelimelerin (lexeme'lerin) yanında hangi bölümde (A, B, C ve D) oldukları da belirtilmiştir. Türkçe köklere ayırmanın bazı gariplikleri vardır:

deneme=# select to_tsvector('turkish', 'cümle');
to_tsvector
-------------
'ç':1
(1 row)

Bundan dolayı 'ç' harfi arandığında içinde 'cümle' kelimesi geçen satırlar getirilecektir. Belki biraz daha az garip örnekler:

deneme=# select to_tsquery('turkish', 'kelime');
to_tsquery
------------
'kel'
(1 row)

deneme=# select to_tsquery('turkish', 'araba');
to_tsquery
------------
'arap'
(1 row)

Son örnekte görüldüğü üzere 'arap' kelimesi arandığında içinde 'araba' geçen satırlar da eşlenecektir. Varsayılan olarak Türkçe arama yapılması için:

deneme=> set default_text_search_config=turkish;

komutu kullanılabilir.

tsvector yapılarında lexeme yanında bulunan pozisyon bilgileri "proximity ranking" sıralaması için kullanılmaktadır.

Dillerde aramaya katmanın pek mantıklı olmadığı kelimelere stop words denmektedir (Türkçe'de 'bu', 'şu', 've', 'mesela' gibi..) Bu kelimeler /usr/share/postgresql/8.3/tsearch_data/ benzeri bir klasörde bulunmaktadır, buraya aramada kullanılmasını istemediğiniz yeni kelimeleri ekleyebilirsiniz.

Eğer Türkçe arama yapmak istemiyorsanız, yeni bir sözlük dizisi oluşturmanız gerekir. Bu konuya daha sonra tekrar değineceğim.

Tam metin arama yollarından ikincisi, yani ayrı bir tablo alanında tsvector bilgilerini tutma tekniğini gördük. Şimdi anlatacağımız bir numaralı teknik ise direk index oluşturmaktan geçiyor:

deneme=# CREATE INDEX metin_idx1 ON metin USING gin((
setweight(to_tsvector('turkish', coalesce(norm_text(baslik), '')), 'A') ||
setweight(to_tsvector('turkish', coalesce(norm_text(anahtar_kelimeler), '')), 'B') ||
setweight(to_tsvector('turkish', coalesce(norm_text(ozet), '')), 'C') ||
setweight(to_tsvector('turkish', coalesce(norm_text(tam_yazi), '')), 'D')));

CREATE INDEX

Artık baslik, anahtar kelimeler, ozet ve tam_yazi alanları üzerinde tanımlı bir indeksleme mekanizması devrededir. Bu sayede büyük veritabanları için devasa verimlilik artışları mümkündür. Esasında önceki teknikte verilen fazladan kolon üzerinde de index tanımı mümkündür, bunun yanında tek tek diğer kolonlar üzerinde index oluşturmak da bu kolonlarda tam metin arama yapılacaksa faydalı olabilir:

CREATE INDEX metin_baslik_idx ON metin USING gin(
to_tsvector('basit', norm_text(baslik)));

CREATE INDEX metin_ozet_idx ON metin USING gin(
to_tsvector('basit', norm_text(ozet)));

CREATE INDEX metin_anahtarlar_idx ON metin USING gin(
to_tsvector('basit', norm_text(anahtar_kelimeler)));

CREATE INDEX metin_tam_yazi_idx ON metin USING gin(
to_tsvector('basit', norm_text(tam_yazi)));

CREATE INDEX metin_yazar_idx ON metin USING gin(
to_tsvector('basit', norm_text(yazar)));

Burada dikkat edilirse 'basit' isimli bir sözlük zincirini kullandık, 'turkish' yerine. Bunu daha tanımlamadığımız için hata almış olabilirsiniz. Eğer Türkçe eklere ayırma gibi özellikleri istemiyorsanız yeni bir basit sözlük zinciri oluşturmanız faydalı olur:

deneme=> CREATE TEXT SEARCH DICTIONARY public.simple_turk (
TEMPLATE = pg_catalog.simple,
STOPWORDS = turkish
);

deneme=> CREATE TEXT SEARCH CONFIGURATION public.basit (COPY = pg_catalog.turkish);

deneme=> alter text search configuration basit alter mapping for asciihword, asciiword, hword, hword_asciipart, hword_part, word with simple_turk;

Tüm bunları yaptıktan sonra basit isimli yeni bir sözlük zincirimiz yaratıldı. Bu sözlük kümesi pek bir kök ayrımı yapmıyor, bunu şu şekilde test edebiliriz:

deneme=# SELECT * FROM ts_debug('public.basit', 'fdeenem tayfun türkçe mi birşey üğöç');
alias | description | token | dictionaries | dictionary | lexemes
-----------+-------------------+---------+---------------+-------------+-----------
asciiword | Word, all ASCII | fdeenem | {simple_dict} | simple_dict | {fdeenem}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | tayfun | {simple_dict} | simple_dict | {tayfun}
blank | Space symbols | | {} | |
word | Word, all letters | türkçe | {simple_dict} | simple_dict | {türkçe}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | mi | {simple_dict} | simple_dict | {mi}
blank | Space symbols | | {} | |
word | Word, all letters | birşey | {simple_dict} | simple_dict | {}
blank | Space symbols | | {} | |
word | Word, all letters | üğöç | {simple_dict} | simple_dict | {üğöç}
(11 rows)

Bundan sonra şu şekilde daha karmaşık aramalar yapabiliriz:

deneme=> select * from metin where to_tsvector('basit', norm_text(tam_yazi)) @@ to_tsquery('basit', norm_text('ab')) AND to_tsvector('basit', norm_text(anahtar_kelimeler)) @@ to_tsquery('basit', norm_text('politikacı'));

Her alanda belirli kelimeleri bulmak bu şekilde daha hızlı ve kolay olacaktır çünkü her alanda index'imiz vardır.


Eğer varsayılan tam metin arama dili olarak Türkçe'yi kullanmak istiyorsanız ('turkish' olarak verilen argüman yerine) bu ayarı direk veritabananı için düzenleyebilirsiniz:

deneme=# alter database deneme set default_text_search_config = turkish;

EXPLAIN ve EXPLAIN ANALYZE ile Testler


Explain komutunu kullanarak bir sql sorgusunun tahmini nasıl çalıştırılacağını görebilirsiniz. 'explain analyze' ise sorguyu gerçekten çalıştırır ve sonuçları verir. Sorgu planlayıcısına (query planner) komut vererek indexler varken sequential scan yapmamasını sağlayabilirsiniz:

Explain kullandığınızda index yaratmamıza rağmen halen seq. scan (sıralı tarama) yapıldığını görüp şaşırabilirsiniz:

deneme=# explain select * from metin where to_tsvector('basit', norm_text(tam_yazi)) @@ to_tsquery('basit', norm_text('ab'));
QUERY PLAN
---------------------------------------------------------------------------------------
Seq Scan on metin (cost=0.00..2.79 rows=1 width=336)
Filter: (to_tsvector('basit'::regconfig, norm_text(tam_yazi)) @@ '''ab'''::tsquery)
(2 rows)

İşin aslı şu ki, sorgu planlayıcımız tabloda az miktarda veri olduğunu görüyor ve index kullanmak (ağaç yapıları ve hash kodlaması) yerine sıralı bir şekilde teker teker kontrol etmenin daha hızlı olacağını düşünüyor. Aslında yanılıyor da sayılmaz, bizim veritabanımız çok küçük, adam hash hesaplayana veya ağaç (B-tree?) ile uğraşana kadar hepsini teker teker kontrol edebilir. Biz testlerde bunun böyle olmamasını istiyoruz. Bunun için manuel olarak sıralı tarama özelliğini kapatabiliriz:

deneme=> set enable_seqscan = false ;
deneme=> show enable_seqscan;

Bu komut ile sorgu planlayıcının elinden sıralı tarama aracını almış oluyoruz, o da artık diğer alternatiflere bakmak zorunda. Yukarıda Seq. Scan yapılan sorguyu aynen tekrarladığımızda:

deneme=# set enable_seqscan = false;
SET
deneme=# explain select * from metin where to_tsvector('basit', norm_text(tam_yazi)) @@ to_tsquery('basit', norm_text('ab'));
QUERY PLAN
-------------------------------------------------------------------------------------------
Index Scan using metin_tam_yazi_idx on metin (cost=0.25..8.52 rows=1 width=336)
Index Cond: (to_tsvector('basit'::regconfig, norm_text(tam_yazi)) @@ '''ab'''::tsquery)
(2 rows)

Süper, index'imiz veritabanımız büyüdükçe sorgu planlayıcı tarafından daha da kullanılacak.


Sıralama ve İşaretlemeler



Peki herhangi bir aramada sıralamayı nasıl değiştirebiliriz? Ve hangi alanların eşleştiğiini nasıl öğreniriz?

Sıralama konusu basitce şöyle:

deneme=# select baslik, ts_rank(lexemes, to_tsquery('basit', 'Turkiye')) as rank from metin where lexemes @@ to_tsquery('basit', 'Turkiye');
baslik | rank
---------------------------+-----------
İhracat yüzde 35 geriledi | 0.0759909
(1 row)

Burada tek yapılan ts_rank fonksiyonu ile tsvector ve aranılan sorgu kelimeleri arasındaki ilişki puanını seçmek. Bu basit örnek kullanılarak puana göre sıralama yapılabilir.

Bunun dışında bir web sayfasında ilişki kurulan veri ile sorgunun nerelerde eşleştiğini işaretlemek sıkça kullanılan bir yöntem. Örneğin google, sorgularında eşlenen kelimeleri kalın harflerle veya sarı arka planda gösterebiliyor. Bunu PostgreSQL de -biraz sınırlı olsa da- yapabiliyor. ts_headline fonksiyonu bu işe yarıyor:

deneme=# select ts_headline('basit', tam_yazi, to_tsquery('basit', 'mesela')) from metin where lexemes @@ to_tsquery('basit', 'mesela');
ts_headline
---------------------------------------------------------------------------------------------------------------------
Mesela bazı ilçeler var ki, henüz altyapıları, kanalizasyon sorunları bile büyük ölçüde çözülmedi. Ben bizim
(1 row)

ts_headline fonksiyonunun çok ilginç özellikleri var. Varsayılan olarak <b> ile etiketlenen eşleşmeyi değiştirebilirsiniz, kaç tane kelimenin verileceğini ayarlayabilirsiniz.

ts_headline fonksiyonu arka planda tsvector verisini kullanMIyor. tsvector'de pozisyon da tutulduğu için insan aksini düşünebilir ama ts_headline her seferinde baştan eşleşme yerlerini buluyor. Bu yüzden çok hızlı olduğu söylenemez. Eğer sizin istediğiniz esneklikte değilse başka bir yöntem ile (PHP?) işaretleme sağlanabilir.

Sınırlamalar


tsvector veri yapısının büyüklüğü en fazla 1 MB olmalı ve tsvector'deki pozisyon bilgisi 16383'den büyük olmamalı, yani bir dökümanda en fazla 16383 tane kelime bulunabilir.

Eğer PostgreSQL'in tam metin arama yetenekleri sizi kesmezse daha esnek olan Sphinx'i deneyebilirsiniz.


Kolay gelsin.



Referanslar:
PostgreSQL resmi dökümanları, örneğin: http://www.postgresql.org/docs/8.3/interactive/textsearch-intro.html
Tam metin sorgu bölümü için: http://www.postgresql.org/docs/8.3/interactive/textsearch.html