Indeksowanie i wyszukiwanie plików pod Linuksem

Na jednym serwerów, którym zarządzałem przechowywane były kopie zapasowe plików. Zajmowały ok. 1,5 TB (ponad 4 mln plików) miejsca na dyskach. Przeszukiwanie tak dużego zbioru plików przy pomocy polecenia „find” jest dość uciążliwe, wolne oraz nie każdy użytkownik posiada taką możliwość. Nie znalazłem aplikacji do indeksowania oraz wyszukiwania plików pod Linuksa, która spełniałaby nasze wymagania, więc postanowiłem wykonać własny mechanizm.

Na początek określimy wymagania wyszukiwarki. Aplikacja powinna:

  • indeksować pliki w czasie rzeczywistym, czyli wykrywać i reagować na dodawanie, usuwanie oraz modyfikacje plików,
  • umożliwiać wyszukiwanie plików według nazwy i ścieżki oraz inny atrybutów np. daty ostatniej modyfikacji, rozmiar itd.,
  • generować podpowiedzi dla wpisywanych fraz oraz wyszukiwać na podstawie niepełnych fraz,
  • sortować wyniki według trafności,
  • posiadać interfejs dostępny przez WWW,
  • posiadać możliwość nadawania uprawnień do przeszukiwania określonych lokalizacji.

Początkowy plan zakładał wykorzystanie narzędzia „inotify” do monitorowania plików. Skrypt wykrywający zmiany na dysku (Python) indeksowałby pliki i zapisywał dane do bazy (MySQL). Mając dane w bazie można uruchomiać wyszukiwanie pełnotekstowe (Sphinx) i udostępniać wyniki przez WWW (PHP).

Potrzebne narzędzia instalujemy przy pomocy apt:

apt-get install inotify-tools python-pyinotify

Najprostszy skrypt do monitorowania plików może wyglądać tak:

#!/usr/bin/python

import os
import pyinotify
import MySQLdb

from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent

wm = WatchManager()
mask = pyinotify.IN_ACCESS | pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY

# Connect to database.
conn = MySQLdb.connect (host = "localhost", user = "myuser", passwd = "mypassword", db = "search")

class PTmp(ProcessEvent):
  def process_IN_CREATE(self, event):
    # Begin transaction.
    conn.begin()
    c = conn.cursor()

    # Try to add file.
    name = os.path.join(event.path, event.name)
    c.execute("INSERT IGNORE INTO files (name) VALUES (%s)", (name))

    # Get file ID.
    c.execute("SELECT id FROM files WHERE name == %s", (file))
    fileId = c.fetchone()[0]

    # Log operation.
    c.execute("INSERT INTO files_history (file_id, time, operation) VALUES(%s, NOW(), %s)", (fileId, "CREATE"))
    conn.commit()

  def process_IN_DELETE(self, event):
    # Begin transaction.
    conn.begin()
    c = conn.cursor()

    # Try to add file.
    name = os.path.join(event.path, event.name)
    c.execute("INSERT IGNORE INTO files (name) VALUES (%s)", (name))

    # Get file ID.
    c.execute("SELECT id FROM files WHERE name == %s", (file))
    fileId = c.fetchone()[0]

    # Log operation.
    c.execute("INSERT INTO files_history (file_id, time, operation) VALUES(%s, NOW(), %s)", (fileId, "DELETE"))
    conn.commit()

  def process_IN_MODIFY(self, event):
    # Begin transaction.
    conn.begin()
    c = conn.cursor()

    # Try to add file.
    name = os.path.join(event.path, event.name)
    c.execute("INSERT IGNORE INTO files (name) VALUES (%s)", (name))

    # Get file ID.
    c.execute("SELECT id FROM files WHERE name == %s", (file))
    fileId = c.fetchone()[0]

    # Log operation.
    c.execute("INSERT INTO files_history (file_id, time, operation) VALUES(%s, NOW(), %s)", (fileId, "MODIFY"))
    conn.commit()

notifier = Notifier(wm, PTmp())
wdd = wm.add_watch('/backup', mask, rec=True)
wdd = wm.add_watch('/home', mask, rec=True)

while True:
  try:
    # process the queue of events as explained above
    notifier.process_events()
    if notifier.check_events():
      # read notified events and enqeue them
      notifier.read_events()
    # you can do some tasks here...
  except KeyboardInterrupt:
    # destroy the inotify's instance on this interrupt (stop monitoring)
    notifier.stop()
    break

Skrypt monitoruje określone katalogi i zapisuje do bazy pliki oraz historię zmian. Niestety, szybko okazało się, że monitorowanie tak dużej liczby plików nie jest możliwe przy pomocy inotify. Próba uruchomienia skryptu kończy się komunikatem o wyczerpanym limicie obserwatorów:

[Pyinotify ERROR] add_watch: cannot watch /some/file.txt (WD=-1)

Limit można podnieść, ale ustawianie limitu na 5 mln nie wydaje się rozsądne, a uruchamianie powyższego skryptu trwa bardzo długo.

Podobny efekt możemy uzyskać przy pomocy narzędzia „inotifywait”:

inotifywait --monitor --recursive --event CREATE --event DELETE --event MODIFY /backup

Jednak uruchomienie polecenia kończy się podobnym komunikatem:

Failed to watch /backup; upper limit on inotify watches reached!
Please increase the amount of inotify watches allowed per user via `/proc/sys/fs/inotify/max_user_watches'.

Jak widać monitorowaniu dużej liczby plików przy pomocy inotify może być kłopotliwe.

Do chwili obecnej nie znalazłem jeszcze wydajnego sposobu na monitorowanie zmian w tak dużej liczbie plików w czasie rzeczywistym.

Statystyki

Indeksowanie przy pomocy python, find itd. zajęło ok. godzinę (1 h 5 min.). Tabela zawiera 4097484 rekordów i zajmuje ok. 1,3 GB.

Indeksowanie Sphinksem zajęło 39 min. 35 s., a łączy rozmiar wszystkich plików zawierających dane wynosi 9,8 GB (podczas indeksowania pliki tymczasowe zajmowały ponad 15 GB).

Linki

http://www.ibm.com/developerworks/linux/library/l-ubuntu-inotify/index.html

http://pyinotify.sourceforge.net

http://people.gnome.org/~veillard/gamin/python.html

Ten wpis został opublikowany w kategorii Linux, MySQL, PHP, Projekty, Python. Dodaj zakładkę do bezpośredniego odnośnika.