🔍 Tworzenie lokalnej wyszukiwarki AI na Linuxie - Kompleksowy przewodnik

Lokalne wyszukiwarki oparte na sztucznej inteligencji rewolucjonizują sposób, w jaki wyszukujemy i analizujemy dane bez uzależniania się od usług chmurowych. W tym kompleksowym przewodniku pokazujemy, jak zbudować i wdrożyć własną, w pełni prywatną wyszukiwarkę AI w środowisku Linux, która rozumie kontekst i znaczenie zapytań, a nie tylko dopasowuje słowa kluczowe.

⚡ Ekspresowe Podsumowanie:

  1. Przewaga wyszukiwarek semantycznych: Wyszukiwarki AI rozumieją znaczenie zapytań, a nie tylko dopasowują słowa kluczowe, co daje znacznie lepsze wyniki.
  2. Pełna prywatność: Dzięki lokalnemu hostingowi, wszystkie dane i zapytania pozostają pod Twoją kontrolą, bez wysyłania informacji na zewnętrzne serwery.
  3. Komponenty wyszukiwarki: Skuteczna implementacja wymaga lokalnego modelu LLM, bazy danych wektorowych i interfejsu użytkownika.
  4. Minimalne wymagania: Nawet komputer z 16GB RAM i nowoczesnym CPU może obsługiwać podstawową wyszukiwarkę AI dla osobistych potrzeb.

🗺️ Spis Treści - Twoja Mapa Drogowa


📚 Podstawy wyszukiwania semantycznego

Zanim przejdziemy do praktycznej implementacji, warto zrozumieć fundamentalne koncepcje stojące za wyszukiwaniem semantycznym opartym na AI.

Wyszukiwanie tradycyjne vs. semantyczne

Tradycyjne wyszukiwarki (takie jak starsze wersje Apache Lucene czy Elasticsearch) działają głównie na zasadzie dopasowywania słów kluczowych:

  • Indeksują słowa pojawiające się w dokumentach
  • Używają metryk statystycznych jak TF-IDF (term frequency-inverse document frequency)
  • Nie rozumieją kontekstu - "bank finansowy" i "bank rzeki" są traktowane podobnie
  • Wymagają dokładnego dopasowania słów - synonimy są problematyczne

Wyszukiwanie semantyczne natomiast:

  • Rozumie znaczenie i kontekst - chwyta intencję zapytania
  • Wykorzystuje osadzenia wektorowe (embeddings) do reprezentacji tekstu
  • Rozpoznaje powiązania semantyczne między pojęciami
  • Znajduje odpowiedź nawet gdy dokładne słowa nie występują w dokumentach

Jak działają embeddingi?

Osadzenia wektorowe (embeddings) to serce wyszukiwania semantycznego:

  1. Modele neuronowe przetwarzają tekst na wektory liczbowe (np. 384 lub 768 wymiarów)
  2. Słowa i pojęcia o podobnym znaczeniu mają podobne wektory w przestrzeni
  3. Podobieństwo między zapytaniem a dokumentem jest mierzone przez odległość w przestrzeni wektorowej (np. podobieństwo kosinusowe)
  4. Model uczy się kontekstu z ogromnych zbiorów tekstów
# Przykład obliczania podobieństwa kosinusowego między wektorami embeddings
import numpy as np

def cosine_similarity(vec_a, vec_b):
    """Oblicza podobieństwo kosinusowe między dwoma wektorami."""
    dot_product = np.dot(vec_a, vec_b)
    norm_a = np.linalg.norm(vec_a)
    norm_b = np.linalg.norm(vec_b)
    return dot_product / (norm_a * norm_b)

# Przykładowe wektory embeddings (zwykle mają setki wymiarów)
query_embedding = np.array([0.2, 0.5, 0.1, 0.8])
document_embedding = np.array([0.3, 0.4, 0.2, 0.7])

similarity = cosine_similarity(query_embedding, document_embedding)
print(f"Podobieństwo: {similarity:.4f}")  # Im bliżej 1.0, tym większe podobieństwo

Rodzaje baz danych wektorowych

Wyszukiwanie semantyczne wymaga specjalistycznych baz danych zoptymalizowanych pod kątem zapytań wektorowych:

  • Milvus - skalowalna, open-source platforma do podobieństwa wektorowego
  • Qdrant - wysoko wydajna baza wektorowa napisana w Rust
  • Weaviate - grafowa baza danych wektorowych dla AI
  • Chroma - lekka, wbudowana baza danych dla aplikacji AI
  • FAISS (Facebook AI Similarity Search) - biblioteka do wydajnego wyszukiwania podobieństwa

✨ Pro Tip: Wybór bazy danych wektorowej powinien zależeć od skali projektu. Dla osobistej wyszukiwarki Chroma będzie najprostszym rozwiązaniem, podczas gdy dla większych aplikacji warto rozważyć Qdrant lub Milvus, które oferują lepszą skalowalność i wydajność.

🖥️ Przygotowanie środowiska Linux

Pierwszym krokiem do stworzenia lokalnej wyszukiwarki AI jest odpowiednie przygotowanie środowiska Linux.

Wymagania sprzętowe

Do uruchomienia lokalnej wyszukiwarki AI potrzebujesz:

Komponent Minimum Zalecane
CPU 4 rdzenie, obsługa AVX2 8+ rdzeni, obsługa AVX2/AVX512
RAM 16 GB 32+ GB
Dysk 20 GB wolnego miejsca (SSD) 100+ GB (NVMe SSD)
GPU (opcjonalnie) - NVIDIA z 8+ GB VRAM

Uwaga: Wymagania zależą głównie od wielkości modelu LLM, którego chcesz użyć. Lżejsze modele jak Phi-2 mogą działać nawet na 8GB RAM, podczas gdy większe modele jak Llama 2 70B wymagają znacznie więcej zasobów.

Instalacja niezbędnych pakietów

Zaczniemy od instalacji podstawowych zależności:

# Dla dystrybucji opartych na Debianie (Ubuntu, Linux Mint, itp.)
sudo apt update
sudo apt install -y python3 python3-pip python3-venv git build-essential cmake

# Dla dystrybucji opartych na RHEL (Fedora, Rocky Linux, itp.)
sudo dnf install -y python3 python3-pip python3-virtualenv git gcc gcc-c++ cmake

# Dla Arch Linux
sudo pacman -S python python-pip python-virtualenv git base-devel cmake

Następnie stwórzmy dedykowane środowisko wirtualne dla naszego projektu:

# Utwórz folder projektu
mkdir ~/ai-search-engine
cd ~/ai-search-engine

# Utwórz wirtualne środowisko Python
python3 -m venv venv
source venv/bin/activate

# Zainstaluj podstawowe biblioteki Python
pip install -U pip setuptools wheel
pip install numpy pandas transformers sentence-transformers torch llama-index
pip install chromadb qdrant-client fastapi uvicorn

Instalacja Ollama dla lokalnych modeli LLM

Ollama to narzędzie, które umożliwia łatwe uruchamianie różnych modeli LLM lokalnie. Zainstalujmy je:

# Instalacja Ollama
curl -fsSL https://ollama.ai/install.sh | sh

# Sprawdzenie, czy Ollama działa
ollama --version

Po instalacji pobierzmy lekki model, który posłuży do generowania odpowiedzi:

# Pobierz model Phi-2 (lekki model Microsoft, dobry do uruchamiania na CPU)
ollama pull phi

# Alternatywnie, jeśli masz więcej zasobów, możesz pobrać Llama 2
# ollama pull llama2

✅ Checklista przygotowania środowiska:

  • 🔍 Sprawdź, czy Twój CPU wspiera instrukcje AVX2 (lscpu | grep avx2)
  • 🔄 Upewnij się, że masz wystarczająco dużo wolnego miejsca na dysku (df -h)
  • 🔒 Zweryfikuj wersję Pythona (zalecana 3.10+) (python3 --version)
  • 📊 Sprawdź dostępną pamięć RAM (free -h)
  • 💾 Opcjonalnie: sprawdź, czy sterowniki GPU są poprawnie zainstalowane (jeśli używasz GPU)

🧠 Implementacja wyszukiwarki semantycznej

Po przygotowaniu środowiska czas na budowę głównych komponentów naszej wyszukiwarki.

Tworzenie indeksera dokumentów

Najpierw stworzymy skrypt, który będzie przetwarzał dokumenty i tworzył osadzenia wektorowe:

# indexer.py
import os
import glob
from typing import List, Dict, Any

from llama_index.core import SimpleDirectoryReader, Document
from llama_index.core import Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import VectorStoreIndex
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import StorageContext
import chromadb

# Konfiguracja modelu embeddingów
# Używamy wielojęzycznego modelu, który dobrze radzi sobie z językiem polskim
embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
Settings.embed_model = embed_model

def load_documents(directory: str) -> List[Document]:
    """Ładuje dokumenty z podanego katalogu."""
    # Obsługiwane formaty: .txt, .pdf, .docx, .md
    documents = SimpleDirectoryReader(
        input_dir=directory,
        recursive=True
    ).load_data()
    print(f"Załadowano {len(documents)} dokumentów.")
    return documents

def create_index(documents: List[Document], persist_dir: str) -> VectorStoreIndex:
    """Tworzy indeks wektorowy z dokumentów i zapisuje go lokalnie."""
    # Inicjalizacja klienta Chroma
    chroma_client = chromadb.PersistentClient(path=persist_dir)
    chroma_collection = chroma_client.get_or_create_collection("documents")

    # Tworzenie vector store
    vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
    storage_context = StorageContext.from_defaults(vector_store=vector_store)

    # Tworzenie indeksu
    index = VectorStoreIndex.from_documents(
        documents,
        storage_context=storage_context,
    )

    return index

if __name__ == "__main__":
    # Katalog z dokumentami do zaindeksowania
    docs_directory = "data/documents"
    # Katalog do przechowywania indeksu
    persist_directory = "data/index"

    # Utwórz katalogi, jeśli nie istnieją
    os.makedirs(docs_directory, exist_ok=True)
    os.makedirs(persist_directory, exist_ok=True)

    # Sprawdź, czy są jakieś dokumenty do zaindeksowania
    if not glob.glob(f"{docs_directory}/**/*.*", recursive=True):
        print(f"Brak dokumentów w katalogu {docs_directory}. "
              f"Dodaj pliki przed indeksowaniem.")
        exit(1)

    # Załaduj dokumenty i utwórz indeks
    documents = load_documents(docs_directory)
    index = create_index(documents, persist_directory)

    print(f"Indeksowanie zakończone. Indeks zapisany w {persist_directory}")

Tworzenie silnika wyszukiwania

Teraz stwórzmy silnik wyszukiwania, który będzie używał lokalnego modelu Ollama i indeksu wektorowego:

# search_engine.py
import os
from typing import List, Dict, Any

from llama_index.core import Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.ollama import Ollama
from llama_index.core import VectorStoreIndex
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import StorageContext
import chromadb

# Konfiguracja modeli
embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
llm = Ollama(model="phi", request_timeout=120.0)
Settings.embed_model = embed_model
Settings.llm = llm

class SemanticSearchEngine:
    def __init__(self, persist_dir: str = "data/index"):
        """Inicjalizuje silnik wyszukiwania semantycznego."""
        self.persist_dir = persist_dir
        self.index = self._load_index()
        self.query_engine = self._create_query_engine()

    def _load_index(self) -> VectorStoreIndex:
        """Ładuje indeks wektorowy z dysku."""
        if not os.path.exists(self.persist_dir):
            raise ValueError(f"Indeks nie istnieje w {self.persist_dir}. "
                           "Uruchom najpierw indexer.py.")

        # Inicjalizacja klienta Chroma
        chroma_client = chromadb.PersistentClient(path=self.persist_dir)
        chroma_collection = chroma_client.get_or_create_collection("documents")

        # Tworzenie vector store
        vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
        storage_context = StorageContext.from_defaults(vector_store=vector_store)

        # Ładowanie indeksu
        index = VectorStoreIndex.from_vector_store(
            vector_store,
            storage_context=storage_context,
        )

        return index

    def _create_query_engine(self):
        """Tworzy silnik zapytań z załadowanego indeksu."""
        return self.index.as_query_engine(
            similarity_top_k=5,  # Liczba najlepszych dokumentów do rozważenia
            response_mode="compact"  # "compact" dla krótszych odpowiedzi
        )

    def search(self, query: str) -> Dict[str, Any]:
        """Wykonuje wyszukiwanie semantyczne na podstawie zapytania."""
        try:
            result = self.query_engine.query(query)

            # Przygotowanie odpowiedzi
            response = {
                "answer": str(result),
                "sources": []
            }

            # Dołączenie źródeł, jeśli są dostępne
            if hasattr(result, "source_nodes"):
                for i, node in enumerate(result.source_nodes):
                    response["sources"].append({
                        "text": node.node.text,
                        "score": node.score,
                        "document": node.node.metadata.get("file_name", "Nieznane źródło")
                    })

            return response
        except Exception as e:
            return {"error": str(e), "answer": "", "sources": []}

if __name__ == "__main__":
    # Test silnika wyszukiwania
    search_engine = SemanticSearchEngine()

    # Przykładowe zapytanie
    query = "Jak działa wyszukiwanie semantyczne?"
    print(f"Zapytanie: {query}")

    result = search_engine.search(query)
    print(f"Odpowiedź: {result['answer']}")

    if result.get("sources"):
        print("\nŹródła:")
        for i, source in enumerate(result["sources"]):
            print(f"{i+1}. {source['document']} (score: {source['score']:.4f})")
            print(f"   Fragment: {source['text'][:100]}...")

Tworzenie prostego API i interfejsu użytkownika

Teraz stwórzmy proste API z FastAPI, które pozwoli nam na interakcję z wyszukiwarką:

# api.py
import os
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
import uvicorn

# Import silnika wyszukiwania
from search_engine import SemanticSearchEngine

# Inicjalizacja aplikacji FastAPI
app = FastAPI(title="Lokalna wyszukiwarka AI")

# Katalog z zasobami statycznymi i szablonami
current_dir = os.path.dirname(os.path.realpath(__file__))
static_dir = os.path.join(current_dir, "static")
templates_dir = os.path.join(current_dir, "templates")

# Tworzenie katalogów, jeśli nie istnieją
os.makedirs(static_dir, exist_ok=True)
os.makedirs(templates_dir, exist_ok=True)

# Tworzenie szablonu HTML
with open(os.path.join(templates_dir, "index.html"), "w") as f:
    f.write("""<!DOCTYPE html>
<html>
<head>
    <title>Lokalna wyszukiwarka AI</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }
        h1 {
            color: #2a6496;
            text-align: center;
        }
        .search-container {
            margin: 30px 0;
        }
        #search-input {
            width: 80%;
            padding: 10px;
            font-size: 16px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        #search-button {
            padding: 10px 15px;
            background-color: #2a6496;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }
        #search-button:hover {
            background-color: #1d4568;
        }
        .result-container {
            margin-top: 30px;
            border-top: 1px solid #eee;
            padding-top: 20px;
        }
        .answer {
            background-color: #f9f9f9;
            padding: 15px;
            border-radius: 4px;
            border-left: 4px solid #2a6496;
        }
        .sources {
            margin-top: 20px;
        }
        .source-item {
            margin-bottom: 15px;
            padding: 10px;
            background-color: #f5f5f5;
            border-radius: 4px;
        }
        .loading {
            text-align: center;
            display: none;
        }
        .error {
            color: #c7254e;
            background-color: #f9f2f4;
            padding: 15px;
            border-radius: 4px;
            display: none;
        }
    </style>
</head>
<body>
    <h1>Lokalna wyszukiwarka AI</h1>

    <div class="search-container">
        <input type="text" id="search-input" placeholder="Wpisz swoje zapytanie...">
        <button id="search-button">Szukaj</button>
    </div>

    <div class="loading" id="loading">Wyszukiwanie w toku...</div>
    <div class="error" id="error"></div>

    <div class="result-container" id="result-container" style="display: none;">
        <h2>Odpowiedź:</h2>
        <div class="answer" id="answer"></div>

        <h3>Źródła:</h3>
        <div class="sources" id="sources"></div>
    </div>

    <script>
        document.getElementById('search-button').addEventListener('click', performSearch);
        document.getElementById('search-input').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                performSearch();
            }
        });

        function performSearch() {
            const query = document.getElementById('search-input').value.trim();
            if (!query) return;

            // Pokazanie ładowania
            document.getElementById('loading').style.display = 'block';
            document.getElementById('result-container').style.display = 'none';
            document.getElementById('error').style.display = 'none';

            // Wywołanie API
            fetch('/api/search', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ query })
            })
            .then(response => response.json())
            .then(data => {
                // Ukrycie ładowania
                document.getElementById('loading').style.display = 'none';

                if (data.error) {
                    // Wyświetlenie błędu
                    document.getElementById('error').textContent = 'Błąd: ' + data.error;
                    document.getElementById('error').style.display = 'block';
                } else {
                    // Wyświetlenie wyniku
                    document.getElementById('answer').textContent = data.answer;

                    // Wyświetlenie źródeł
                    const sourcesElement = document.getElementById('sources');
                    sourcesElement.innerHTML = '';

                    if (data.sources && data.sources.length > 0) {
                        data.sources.forEach((source, index) => {
                            const sourceElement = document.createElement('div');
                            sourceElement.className = 'source-item';
                            sourceElement.innerHTML = `
                                <strong>${index + 1}. ${source.document}</strong> (score: ${source.score.toFixed(4)})
                                <p>${source.text.substring(0, 200)}...</p>
                            `;
                            sourcesElement.appendChild(sourceElement);
                        });
                    } else {
                        sourcesElement.innerHTML = '<p>Brak źródeł.</p>';
                    }

                    document.getElementById('result-container').style.display = 'block';
                }
            })
            .catch(error => {
                document.getElementById('loading').style.display = 'none';
                document.getElementById('error').textContent = 'Błąd podczas wyszukiwania: ' + error;
                document.getElementById('error').style.display = 'block';
            });
        }
    </script>
</body>
</html>
""")

# Konfiguracja statycznych plików i szablonów
app.mount("/static", StaticFiles(directory=static_dir), name="static")
templates = Jinja2Templates(directory=templates_dir)

# Model danych dla zapytań
class SearchQuery(BaseModel):
    query: str

# Inicjalizacja silnika wyszukiwania
try:
    search_engine = SemanticSearchEngine()
except ValueError as e:
    # Gdy nie znaleziono indeksu, ustawiamy na None i wyświetlimy komunikat o błędzie
    print(f"Error: {e}")
    print("API zostanie uruchomione, ale wyszukiwanie nie będzie działać. "
          "Uruchom najpierw indexer.py.")
    search_engine = None

@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
    """Renderuje stronę główną."""
    return templates.TemplateResponse("index.html", {"request": request})

@app.post("/api/search")
async def search(query: SearchQuery):
    """Endpoint API do wyszukiwania."""
    if search_engine is None:
        raise HTTPException(status_code=503, detail="Silnik wyszukiwania nie jest dostępny. "
                           "Uruchom najpierw indexer.py.")

    result = search_engine.search(query.query)
    return result

if __name__ == "__main__":
    uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=True)

🚀 Uruchomienie i testowanie wyszukiwarki

Po zaimplementowaniu wszystkich komponentów, możemy przystąpić do uruchomienia naszej lokalnej wyszukiwarki AI.

Przygotowanie danych testowych

Najpierw stwórzmy przykładowe dokumenty do wyszukiwania:

# Tworzenie katalogu na dokumenty
mkdir -p data/documents

# Tworzenie przykładowych dokumentów
cat > data/documents/wyszukiwanie_semantyczne.txt << EOL
# Wyszukiwanie Semantyczne

Wyszukiwanie semantyczne to technologia wykorzystująca sztuczną inteligencję do zrozumienia intencji zapytania użytkownika i kontekstu dokumentów. W przeciwieństwie do tradycyjnego wyszukiwania opartego na słowach kluczowych, wyszukiwanie semantyczne analizuje znaczenie całego zapytania.

## Jak działa wyszukiwanie semantyczne?

1. **Przetwarzanie języka naturalnego (NLP)** analizuje zapytanie użytkownika, aby zrozumieć jego prawdziwe znaczenie i kontekst.
2. **Modele osadzania (embedding models)** przekształcają tekst na wektory liczbowe reprezentujące jego znaczenie.
3. **Bazy danych wektorowe** przechowują wektory osadzenia dla dokumentów i umożliwiają szybkie wyszukiwanie podobieństwa.
4. **Algorytmy podobieństwa** porównują wektor zapytania z wektorami dokumentów, aby znaleźć najbardziej odpowiednie wyniki.

## Zalety wyszukiwania semantycznego

- Zrozumienie intencji, a nie tylko słów kluczowych
- Obsługa synonimów i związanych terminów
- Lepsze wyniki dla złożonych zapytań
- Bardziej naturalne doświadczenie użytkownika
EOL

cat > data/documents/bazy_danych_wektorowe.txt << EOL
# Bazy Danych Wektorowe

Bazy danych wektorowe to wyspecjalizowane systemy zarządzania bazami danych zaprojektowane do przechowywania i wyszukiwania osadzeń wektorowych (vector embeddings). Są fundamentalnym elementem nowoczesnych aplikacji opartych na sztucznej inteligencji, w tym wyszukiwarek semantycznych.

## Dlaczego tradycyjne bazy danych nie wystarczają?

Tradycyjne bazy danych relacyjne i dokumentowe nie są zoptymalizowane do efektywnego przechowywania i wyszukiwania wektorów o wysokiej wymiarowości. Wyszukiwanie podobieństwa w przestrzeni wektorowej wymaga specjalistycznych algorytmów, takich jak:

- Algorytmy przybliżonego najbliższego sąsiada (ANN)
- Indeksy oparte na drzewach KD
- Metody haszowania wrażliwego na lokalność (LSH)

## Popularne bazy danych wektorowe

1. **Milvus** - Open-source platforma do podobieństwa wektorowego
2. **Qdrant** - Wysoko wydajna baza wektorowa napisana w Rust
3. **Weaviate** - Grafowa baza danych wektorowych
4. **Chroma** - Lekka baza danych dla aplikacji AI
5. **Pinecone** - W pełni zarządzana usługa bazodanowa dla wektorów

## Zastosowania

Bazy danych wektorowe są kluczowe dla:
- Wyszukiwania semantycznego
- Systemów rekomendacji
- Wykrywania podobieństw i duplikatów
- Klasyfikacji dokumentów
- Chatbotów i asystentów AI
EOL

cat > data/documents/lokalne_modele_llm.txt << EOL
# Lokalne Modele Językowe (LLM)

Lokalne modele językowe (LLM) umożliwiają uruchamianie zaawansowanych algorytmów sztucznej inteligencji bezpośrednio na komputerze użytkownika, bez konieczności korzystania z usług chmurowych. Jest to kluczowy element dla zachowania prywatności danych i niezależności od zewnętrznych dostawców.

## Zalety lokalnych modeli LLM

- **Prywatność** - dane nie opuszczają urządzenia użytkownika
- **Brak kosztów per zapytanie** - tylko jednorazowy koszt zasobów obliczeniowych
- **Pełna kontrola** - możliwość dostosowania i fine-tuningu
- **Niezależność od internetu** - działanie offline
- **Brak limitów użycia** - nieograniczona liczba zapytań

## Popularne rozwiązania dla lokalnych LLM

1. **Ollama** - proste narzędzie do uruchamiania modeli LLM lokalnie
2. **llama.cpp** - wysoce zoptymalizowana implementacja modeli Llama
3. **Hugging Face Transformers** - biblioteka z szerokim wsparciem dla różnych modeli
4. **Llama Factory** - zestaw narzędzi do dostrajania modeli
5. **LocalAI** - API kompatybilne z OpenAI dla lokalnych modeli

## Wymagania sprzętowe

Wymagania zależą od wielkości modelu:
- Małe modele (1-3B parametrów): 8GB RAM, nowoczesny CPU
- Średnie modele (7-13B parametrów): 16-32GB RAM, mocny CPU lub mały GPU
- Duże modele (30-70B parametrów): 32-64GB RAM, wydajny GPU z 24GB+ VRAM
EOL

Uruchomienie indeksera

Teraz uruchommy nasz indekser, aby przetworzyć i zaindeksować dokumenty:

# Aktywuj środowisko wirtualne, jeśli nie jest aktywne
cd ~/ai-search-engine
source venv/bin/activate

# Uruchom indekser
python indexer.py

# Powinniśmy zobaczyć komunikat o zakończeniu indeksowania

Uruchomienie API i testowanie

Po zaindeksowaniu dokumentów, uruchomimy nasze API:

# Upewnij się, że Ollama działa
ollama list

# Uruchom API
python api.py

Po uruchomieniu API, możesz otworzyć przeglądarkę pod adresem http://localhost:8000 i przetestować wyszukiwarkę.

✨ Pro Tip: Jeśli chcesz, aby Twoja wyszukiwarka była dostępna dla innych urządzeń w sieci lokalnej, uruchom API z parametrem hosta:

python -c "import socket; print(f'IP: {socket.gethostbyname(socket.gethostname())}')"
python api.py --host 0.0.0.0

Następnie możesz uzyskać dostęp do wyszukiwarki z innego urządzenia, wpisując w przeglądarce adres IP Twojego komputera i port 8000.

🔧 Optymalizacja i rozszerzenia

Podstawowa implementacja wyszukiwarki działa, ale możemy ją znacznie ulepszyć poprzez różne optymalizacje i rozszerzenia.

Optymalizacja wydajności

Oto kilka strategii poprawy wydajności naszej wyszukiwarki:

  1. Użycie GPU do obliczeń:
# Modyfikacja search_engine.py, aby korzystać z GPU
import torch

# Sprawdź, czy GPU jest dostępne
if torch.cuda.is_available():
    device = "cuda"
    print(f"Używam GPU: {torch.cuda.get_device_name(0)}")
else:
    device = "cpu"
    print("GPU niedostępne, używam CPU")

# Konfiguracja modeli z użyciem odpowiedniego urządzenia
embed_model = HuggingFaceEmbedding(
    model_name="intfloat/multilingual-e5-large",
    device=device
)
  1. Cache dla zapytań:
# Dodaj cache do silnika wyszukiwania
from functools import lru_cache

class SemanticSearchEngine:
    # ... istniejący kod ...

    @lru_cache(maxsize=100)  # Cache dla 100 ostatnich zapytań
    def search_cached(self, query: str) -> Dict[str, Any]:
        """Wersja search z cache'owaniem."""
        return self.search(query)
  1. Indeksowanie w tle:

Możemy zmodyfikować indekser, aby działał jako usługa w tle, automatycznie aktualizując indeks, gdy pojawiają się nowe dokumenty:

# background_indexer.py
import time
import os
import sys
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

from indexer import load_documents, create_index

class DocumentHandler(FileSystemEventHandler):
    def __init__(self, docs_dir, index_dir):
        self.docs_dir = docs_dir
        self.index_dir = index_dir
        self.pending_update = False
        self.last_update = 0

    def on_any_event(self, event):
        # Ignoruj zdarzenia dla plików tymczasowych
        if event.src_path.endswith('.tmp') or event.src_path.endswith('~'):
            return

        # Ustaw flagę aktualizacji
        self.pending_update = True

    def update_if_needed(self):
        # Aktualizuj indeks nie częściej niż co 60 sekund
        current_time = time.time()
        if self.pending_update and (current_time - self.last_update > 60):
            print(f"Wykryto zmiany w dokumentach. Aktualizuję indeks...")
            try:
                documents = load_documents(self.docs_dir)
                create_index(documents, self.index_dir)
                print(f"Indeks zaktualizowany pomyślnie.")
                self.pending_update = False
                self.last_update = current_time
            except Exception as e:
                print(f"Błąd podczas aktualizacji indeksu: {e}")

def run_background_indexer():
    docs_directory = "data/documents"
    persist_directory = "data/index"

    # Utwórz katalogi, jeśli nie istnieją
    os.makedirs(docs_directory, exist_ok=True)
    os.makedirs(persist_directory, exist_ok=True)

    # Początkowe indeksowanie
    try:
        documents = load_documents(docs_directory)
        create_index(documents, persist_directory)
        print(f"Początkowe indeksowanie zakończone.")
    except Exception as e:
        print(f"Błąd podczas początkowego indeksowania: {e}")

    # Skonfiguruj watchdog
    event_handler = DocumentHandler(docs_directory, persist_directory)
    observer = Observer()
    observer.schedule(event_handler, docs_directory, recursive=True)
    observer.start()

    try:
        while True:
            time.sleep(10)
            event_handler.update_if_needed()
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

if __name__ == "__main__":
    run_background_indexer()

Dodanie wsparcia dla większej liczby formatów dokumentów

Nasza podstawowa implementacja obsługuje głównie pliki tekstowe. Możemy rozszerzyć ją o obsługę innych formatów:

# W indexer.py, zmodyfikuj funkcję load_documents

# Zainstaluj dodatkowe zależności
# pip install pypdf python-docx beautifulsoup4 requests

def load_documents(directory: str) -> List[Document]:
    """Ładuje dokumenty z podanego katalogu z rozszerzonym wsparciem formatów."""
    try:
        # Definiuj dodatkowe parsery dla różnych formatów
        file_extractor = {
            ".pdf": SimpleDirectoryReader.parse_pdf,
            ".docx": SimpleDirectoryReader.parse_docx,
            ".pptx": SimpleDirectoryReader.parse_pptx,
            ".html": SimpleDirectoryReader.parse_html,
            ".md": SimpleDirectoryReader.parse_md,
            ".txt": SimpleDirectoryReader.parse_txt,
        }

        documents = SimpleDirectoryReader(
            input_dir=directory,
            recursive=True,
            file_extractor=file_extractor
        ).load_data()

        print(f"Załadowano {len(documents)} dokumentów.")
        return documents
    except Exception as e:
        print(f"Błąd podczas ładowania dokumentów: {e}")
        raise

Rozszerzenie interfejsu użytkownika

Możemy ulepszyć interfejs użytkownika o dodatkowe funkcje:

  1. Filtry i sortowanie wyników:

Dodaj do szablonu HTML w api.py sekcję z filtrami:

<div class="filters">
    <label for="relevance-threshold">Próg istotności:</label>
    <input type="range" id="relevance-threshold" min="0" max="1" step="0.01" value="0.6">
    <span id="threshold-value">0.6</span>

    <label for="max-results">Maksymalna liczba wyników:</label>
    <select id="max-results">
        <option value="3">3</option>
        <option value="5" selected>5</option>
        <option value="10">10</option>
        <option value="20">20</option>
    </select>
</div>
  1. Dostosowanie endpointu API:
# W api.py, zmodyfikuj endpoint /api/search

@app.post("/api/search")
async def search(query: SearchQuery, max_results: int = 5, threshold: float = 0.6):
    """Endpoint API do wyszukiwania z dodatkowymi parametrami."""
    if search_engine is None:
        raise HTTPException(status_code=503, detail="Silnik wyszukiwania nie jest dostępny.")

    # Wykonaj wyszukiwanie
    result = search_engine.search(query.query)

    # Filtruj wyniki poniżej progu istotności
    if "sources" in result:
        result["sources"] = [
            source for source in result["sources"] 
            if source.get("score", 0) >= threshold
        ][:max_results]

    return result

🔒 Zapewnienie prywatności i bezpieczeństwa

Jedną z głównych zalet lokalnej wyszukiwarki AI jest prywatność. Oto jak możemy ją wzmocnić:

Szyfrowanie indeksu

Możemy dodać szyfrowanie indeksu, aby zapewnić, że nawet w przypadku nieautoryzowanego dostępu do plików, dane pozostaną bezpieczne:

# Zainstaluj dodatkowe zależności
# pip install cryptography

# W indexer.py i search_engine.py, dodaj funkcje szyfrowania/deszyfrowania

from cryptography.fernet import Fernet
import os
import json

def get_or_create_key():
    """Pobiera lub tworzy klucz szyfrowania."""
    key_path = "data/encryption.key"
    if os.path.exists(key_path):
        with open(key_path, "rb") as key_file:
            key = key_file.read()
    else:
        # Generuj nowy klucz
        key = Fernet.generate_key()
        os.makedirs(os.path.dirname(key_path), exist_ok=True)
        with open(key_path, "wb") as key_file:
            key_file.write(key)

    return key

def encrypt_data(data, key):
    """Szyfruje dane JSON."""
    cipher = Fernet(key)
    json_data = json.dumps(data).encode('utf-8')
    encrypted_data = cipher.encrypt(json_data)
    return encrypted_data

def decrypt_data(encrypted_data, key):
    """Deszyfruje dane JSON."""
    cipher = Fernet(key)
    json_data = cipher.decrypt(encrypted_data)
    return json.loads(json_data.decode('utf-8'))

Ograniczenie dostępu do API

Możemy dodać prostą autoryzację do naszego API:

# W api.py, dodaj autoryzację
from fastapi import Depends, HTTPException, status
from fastapi.security import APIKeyHeader

# Definiuj nagłówek API key
API_KEY = "twój-tajny-klucz"  # W rzeczywistości użyj bezpiecznego generatora kluczy
api_key_header = APIKeyHeader(name="X-API-Key")

def verify_api_key(api_key: str = Depends(api_key_header)):
    """Weryfikuje API key."""
    if api_key != API_KEY:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Nieprawidłowy API key"
        )
    return api_key

# Zabezpiecz endpoint API
@app.post("/api/search")
async def search(
    query: SearchQuery,
    api_key: str = Depends(verify_api_key)
):
    # ...reszta kodu...

Ograniczenie zbieranych danych

Możemy zminimalizować ilość danych, które są logowane lub przechowywane:

# W api.py, dodaj kontrolę nad logami
import logging

# Konfiguracja logowania
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[logging.FileHandler("api.log"), logging.StreamHandler()]
)

# Funkcja do anonimizacji danych wrażliwych w logach
def anonymize_query(query: str) -> str:
    """Anonimizuje potencjalnie wrażliwe dane w zapytaniu."""
    import re
    # Anonimizacja potencjalnych numerów telefonów, adresów email, itp.
    query = re.sub(r'\b\d{9,}\b', '[NUMER]', query)
    query = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]', query)
    return query

@app.post("/api/search")
async def search(query: SearchQuery):
    # Loguj tylko anonimizowaną wersję zapytania
    logging.info(f"Otrzymano zapytanie: {anonymize_query(query.query)}")

    # ...reszta kodu...

🏁 Podsumowanie - Korzyści z lokalnej wyszukiwarki AI

Stworzenie własnej lokalnej wyszukiwarki AI przynosi liczne korzyści:

  1. Pełna kontrola nad danymi - wszystko pozostaje na Twoim komputerze, bez wysyłania wrażliwych informacji na zewnętrzne serwery
  2. Brak kosztów subskrypcji - płacisz tylko za swój sprzęt, bez miesięcznych opłat
  3. Nieograniczone wyszukiwania - brak limitów zapytań czy kwot API
  4. Dostosowanie do konkretnych potrzeb - możliwość personalizacji każdego aspektu wyszukiwarki
  5. Działanie offline - wyszukiwarka działa nawet bez dostępu do internetu
  6. Wiedza o technologii AI - praktyczna nauka o działaniu nowoczesnych technologii wyszukiwania

Możliwe zastosowania

Twoja lokalna wyszukiwarka AI może być używana do:

  • Przeszukiwania osobistej bazy wiedzy - notatki, artykuły, pomysły
  • Analizowania dokumentacji technicznej - łatwe znajdowanie odpowiedzi w obszernej dokumentacji
  • Przeszukiwania wewnętrznych dokumentów firmowych - z zachowaniem poufności
  • Personalizowanej bazy badawczej - zarządzanie i wyszukiwanie w publikacjach naukowych
  • Inteligentnego systemu FAQ - odpowiadanie na często zadawane pytania w oparciu o posiadane dokumenty

Dalszy rozwój

Stworzona wyszukiwarka może być rozwijaną platformą do eksperymentów z AI i przetwarzaniem języka naturalnego. Możliwe kierunki rozwoju:

  • Integracja z większymi modelami LLM, gdy będą dostępne lokalnie
  • Dodanie wsparcia dla wyszukiwania wielojęzycznego
  • Implementacja wyszukiwania multimodalnego (tekst + obrazy)
  • Tworzenie spersonalizowanych asystentów AI bazujących na lokalnej wiedzy
  • Automatyczna kategoryzacja i tagowanie dokumentów

🚀 Rozpocznij swoją przygodę z AI już dziś!

Skontaktuj się z IQHost, aby dowiedzieć się więcej o hostowaniu własnych rozwiązań AI

Nasze rozwiązania hostingowe są idealnie dopasowane do uruchamiania własnych aplikacji AI, zapewniając wydajność, bezpieczeństwo i wsparcie techniczne na każdym etapie Twojego projektu.

Czy ten artykuł był pomocny?

Wróć do listy wpisów

Twoja strona WordPress działa wolno?

Sprawdź nasz hosting WordPress z ultraszybkimi dyskami NVMe i konfiguracją serwera zoptymalizowaną pod kątem wydajności. Doświadcz różnicy już dziś!

Sprawdź ofertę hostingu
30-dniowa gwarancja zwrotu pieniędzy