🔍 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:
- Przewaga wyszukiwarek semantycznych: Wyszukiwarki AI rozumieją znaczenie zapytań, a nie tylko dopasowują słowa kluczowe, co daje znacznie lepsze wyniki.
- Pełna prywatność: Dzięki lokalnemu hostingowi, wszystkie dane i zapytania pozostają pod Twoją kontrolą, bez wysyłania informacji na zewnętrzne serwery.
- Komponenty wyszukiwarki: Skuteczna implementacja wymaga lokalnego modelu LLM, bazy danych wektorowych i interfejsu użytkownika.
- 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:
- Modele neuronowe przetwarzają tekst na wektory liczbowe (np. 384 lub 768 wymiarów)
- Słowa i pojęcia o podobnym znaczeniu mają podobne wektory w przestrzeni
- Podobieństwo między zapytaniem a dokumentem jest mierzone przez odległość w przestrzeni wektorowej (np. podobieństwo kosinusowe)
- 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:
- 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
)
- 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)
- 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:
- 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>
- 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:
- Pełna kontrola nad danymi - wszystko pozostaje na Twoim komputerze, bez wysyłania wrażliwych informacji na zewnętrzne serwery
- Brak kosztów subskrypcji - płacisz tylko za swój sprzęt, bez miesięcznych opłat
- Nieograniczone wyszukiwania - brak limitów zapytań czy kwot API
- Dostosowanie do konkretnych potrzeb - możliwość personalizacji każdego aspektu wyszukiwarki
- Działanie offline - wyszukiwarka działa nawet bez dostępu do internetu
- 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?
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