Sign In
Free Sign Up
  • English
  • Español
  • 简体中文
  • Deutsch
  • 日本語
Sign In
Free Sign Up
  • English
  • Español
  • 简体中文
  • Deutsch
  • 日本語

Fortgeschrittene Facebook Event Datenanalyse mit einer Vektordatenbank durchführen

Im digitalen Zeitalter müssen Fachleute in allen Branchen über bevorstehende Veranstaltungen, Konferenzen und Workshops auf dem Laufenden bleiben. Die effiziente Suche nach Veranstaltungen, die den eigenen Interessen entsprechen, stellt jedoch angesichts der Vielzahl von Online-Informationen eine große Herausforderung dar.

Dieser Blog stellt eine innovative Lösung für diese Herausforderung vor: eine umfassende Anwendung, die entwickelt wurde, um Event-Daten von Facebook (opens new window) zu sammeln und die gesammelten Daten mit Hilfe von MyScale (opens new window) zu analysieren. Obwohl MyScale normalerweise mit dem RAG-Technologie-Stack in Verbindung gebracht wird oder als Vektordatenbank verwendet wird, reichen seine Fähigkeiten über diese Bereiche hinaus. Wir werden es für die Datenanalyse nutzen und seine Vektorsuchfunktion verwenden, um semantisch ähnliche Veranstaltungen zu analysieren und so bessere Ergebnisse und Erkenntnisse zu liefern.

Sie werden feststellen, dass Grok AI (opens new window) die Qdrant-Vektordatenbank als Suchmaschine verwendet, um Echtzeitinformationen aus X (ehemals bekannt als Twitter) Daten abzurufen. Sie können auch die Leistungsfähigkeit von Vektordatenbanken auf diese Weise mit MyScale testen, indem Sie MyScale mit anderen Plattformen wie Apify integrieren, um alltägliche Aufgaben durch die Entwicklung einfacher personalisierter Anwendungen zu verbessern.

In diesem Blog entwickeln wir eine Anwendung, die nur den Namen einer Stadt als Eingabe akzeptiert und alle damit verbundenen Veranstaltungen von Facebook sammelt. Anschließend führen wir Datenanalysen und semantische Suche mithilfe der fortschrittlichen SQL-Vektorfunktionen von MyScale durch.

# Tools und Technologien

Wir werden mehrere Tools verwenden, darunter Apify, MyScale (opens new window) und OpenAI, um diese nützliche Anwendung zu entwickeln.

  • Apify: Eine beliebte Web-Scraping (opens new window)- und Automatisierungsplattform, die den Prozess der Datensammlung erheblich vereinfacht. Sie ermöglicht das Scrapen von Daten und deren anschließende Verwendung in LLMs. Dadurch können wir LLMs mit Echtzeitdaten trainieren und Anwendungen entwickeln.
  • MyScale: MyScale ist eine SQL-Vektordatenbank, die wir verwenden, um strukturierte und unstrukturierte Daten auf optimierte Weise zu speichern und zu verarbeiten.
  • OpenAI: Wir werden das Modell text-embedding-3-small von OpenAI (opens new window) verwenden, um die Einbettungen des Textes zu erhalten und diese Einbettungen dann in MyScale für die Datenanalyse und semantische Suche zu speichern.

# Einrichten von MyScale und Apify

Um MyScale und Apify einzurichten, müssen Sie zunächst ein neues Verzeichnis und eine Python (opens new window)-Datei erstellen. Öffnen Sie dazu Ihr Terminal oder die Befehlszeile und geben Sie die folgenden Befehle ein:

mkdir MyScale
cd MyScale
touch main.ipynb

Lassen Sie uns die Pakete installieren. Kopieren Sie den folgenden Befehl und fügen Sie ihn in Ihr Terminal ein. Diese Pakete stellen die Tools und Bibliotheken bereit, die wir für die Entwicklung unserer Anwendung benötigen.

pip install openai apify-client clickhouse-connect pandas numpy

Damit sollten alle Abhängigkeiten in Ihrem System installiert sein. Um zu überprüfen, ob alles ordnungsgemäß installiert ist, geben Sie den folgenden Befehl in Ihr Terminal ein.

pip freeze | egrep '(openai|apify-client|clickhouse-connect|pandas|numpy)'

Dies sollte alle installierten Abhängigkeiten mit ihren Versionen anzeigen. Wenn Ihnen fehlende Abhängigkeiten auffallen, müssen Sie möglicherweise den Installationsbefehl für das jeweilige Paket erneut ausführen. Jetzt sind wir bereit, nach den Installationen unseren Code zu schreiben.

💡 Hinweis: Wir arbeiten in einem Python-Notebook. Betrachten Sie jeden Codeblock als Notebook-Zelle.

# Daten mit Apify scrapen

Jetzt verwenden wir die Apify-API, um Event-Daten von New York City mit dem Facebook Events Scraper (opens new window) zu sammeln.

import pandas as pd
from apify_client import ApifyClient
# Initialisieren Sie den ApifyClient mit Ihrem API-Token
client = ApifyClient("Geben Sie hier Ihren Apify-Schlüssel ein")

# Bereiten Sie die Eingabe für den Actor vor
run_input = {
    "searchQueries": ["Sport New York"],
    "startUrls": [],
    "maxEvents": 50,
}

# Führen Sie den Actor aus und warten Sie, bis er fertig ist
run = client.actor("UZBnerCFBo5FgGouO").call(run_input=run_input)

df_columns = ['Name', 'Datetime', 'Description', 'Users_Going', 'Users_Interested', 'Users_Responded', 'City', 'Organized_By', 'Street_Address']
dataframe1 = pd.DataFrame(columns=df_columns)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    # Verwenden Sie eine Dictionary Comprehension, um None-Werte durch einen leeren String zu ersetzen
    row = {
        'Name': item.get('name', ''),
        'Datetime': item.get('dateTimeSentence', ''),
        'Description': item.get('description', ''),
        'Users_Going': item.get('usersGoing', ''),
        'Users_Interested': item.get('usersInterested', ''),
        'Users_Responded': item.get('usersResponded', ''),
        'City': item.get('location', {}).get('city', '') if item.get('location') else '',
        'Organized_By': item.get('organizedBy', ''),
        'Street_Address': item.get('location', {}).get('streetAddress', '') if item.get('location') else ''
    }
    # Stellen Sie sicher, dass alle None-Werte durch einen leeren String ersetzt werden
    row = {k: (v if v is not None else '') for k, v in row.items()}
    dataframe1 = dataframe1._append(row, ignore_index=True)

# Bereinigen der Daten
dataframe1['Description'] = dataframe1['Description'].replace('\\n', '', regex=True)

Dieses Skript liefert uns die Details zu den bevorstehenden Veranstaltungen in Form eines Pandas DataFrames. 💡 Hinweis: Vergessen Sie nicht, Ihren Apify-API-Schlüssel in das obige Skript einzufügen. Sie finden Ihren API-Token auf der Integrations (opens new window)-Seite in der Apify Console.

# Datenverarbeitung

Wenn wir Rohdaten sammeln, liegen sie in verschiedenen Formaten vor. In diesem Skript bringen wir die Veranstaltungsdaten in ein einheitliches Format, damit unsere Datenfilterung effizienter durchgeführt werden kann.

# Importieren Sie die erforderlichen Bibliotheken für die Datenmanipulation und das Parsen von Datum
import pandas as pd
import numpy as np
from datetime import datetime
from dateutil import parser

# Funktion zum Parsen von Datumszeichenketten, die einen Bereich oder ein einzelnes Datum darstellen können
def parse_dates(date_str):
    # Überprüfen Sie, ob die Datumszeichenkette einen Bindestrich enthält, der auf einen Bereich hinweist
    if '-' in date_str:
        parts = date_str.split('-')
        # Wenn die Zeichenkette in zwei Teile aufgeteilt wird, handelt es sich um einen gültigen Bereich
        if len(parts) == 2:
            try:
                # Start- und Enddaten parsen und formatieren, um ein lesbares Format zu erhalten
                start_date = parser.parse(parts[0], fuzzy=True).strftime('%a, %b %d')
                end_date = parser.parse(parts[1], fuzzy=True).strftime('%a, %b %d')
                return start_date, end_date
            except ValueError:
                # Im Falle eines Parsing-Fehlers nichts tun (wird unten behandelt)
                pass  
    # Wenn es sich nicht um einen Bereich handelt oder das Parsen des Bereichs fehlgeschlagen ist, versuchen Sie, es als einzelnes Datum zu parsen
    try:
        parsed_date = parser.parse(date_str, fuzzy=True)
        # Formatieren Sie das einzelne Datum für start_date und formatieren Sie es für end_date anders
        start_date = parsed_date.strftime('%a, %b %d AT %I:%M %p EDT')
        end_date = parsed_date.strftime('%a, %b %d')  # Zeit für end_date weglassen
        return start_date, end_date
    except ValueError:
        # Geben Sie NaN für beide Daten zurück, wenn das Parsen fehlschlägt
        return np.nan, np.nan  

# Funktion zum Extrahieren von detailliertem Datum, Uhrzeit und Tag aus einer Datumszeichenkette
def extract_date_time_day(date_str):
    try:
        # Parsen der Datumszeichenkette mit einiger Flexibilität beim Eingabeformat
        parsed_date = parser.parse(date_str, fuzzy=True)
        # Datum, Uhrzeit und Tagteile extrahieren und formatieren
        date = parsed_date.strftime('%Y-%m-%d')
        day = parsed_date.strftime('%a')
        # Feststellen, ob die ursprüngliche Zeichenkette einen Zeitbestandteil enthielt
        time_component = parsed_date.strftime('%I:%M %p') not in date_str
        time = parsed_date.strftime('%H:%M:%S') if not time_component else np.nan
    except ValueError:
        # Wenn das Parsen fehlschlägt, setzen Sie Datum, Uhrzeit und Tag auf NaN
        date, time, day = np.nan, np.nan, np.nan
    
    return date, time, day

# Wenden Sie die parse_dates-Funktion auf das DataFrame an und erstellen Sie neue Spalten für Start- und Enddaten
dataframe1[['Start_Date', 'End_Date']] = dataframe1.apply(lambda row: pd.Series(parse_dates(row['Datetime'])), axis=1)

# Löschen Sie Zeilen, in denen Start_Date NaN ist, was darauf hinweist, dass das Parsen nicht erfolgreich war
dataframe = dataframe1.dropna(subset=['Start_Date'])

# Wenden Sie extract_date_time_day an, um die Start- und Enddaten in separate Spalten für Datum, Uhrzeit und Tag aufzuteilen
dataframe['Start_Date'], dataframe['Start_Time'], dataframe['Start_Day'] = zip(*dataframe['Start_Date'].apply(extract_date_time_day))
dataframe['End_Date'], _, dataframe['End_Day'] = zip(*dataframe['End_Date'].apply(extract_date_time_day))

# Entfernen Sie die ursprüngliche 'Datetime'-Spalte, da sie nicht mehr benötigt wird
dataframe=dataframe.drop(['Datetime'], axis=1)

# Konvertieren Sie 'Start_Date' und 'End_Date' in das Datumsformat und extrahieren Sie nur den Datumsanteil
dataframe['Start_Date'] = pd.to_datetime(dataframe['Start_Date']).dt.date
dataframe['End_Date'] = pd.to_datetime(dataframe['End_Date']).dt.date

# Konvertieren Sie 'Start_Time' in das Datumsformat und behalten Sie die Zeitinformationen bei
dataframe['Start_Time'] = pd.to_datetime(dataframe['Start_Time'])

Dieser Codeausschnitt verwendet pandas mit den Paketen datetime und dateutil von Python, um die Daten zu formatieren.

# Einbettungen generieren

Um Veranstaltungen besser zu verstehen und zu durchsuchen, generieren wir Einbettungen aus ihren Beschreibungen mit Hilfe von text-embedding-3-small. Diese Einbettungen erfassen die semantische Essenz jeder Veranstaltung und helfen der Anwendung, bessere Ergebnisse zurückzugeben.

# OpenAI-Bibliothek für den API-Zugriff importieren.
from openai import OpenAI 
# OpenAI-Client mit einem API-Schlüssel initialisieren.
openai_client = OpenAI(api_key="Ihr_OpenAI_API-Schlüssel_hier")
# Funktion zum Abrufen von Texteinbettungen
def get_embedding(text, model="text-embedding-3-small"):
    return openai_client.embeddings.create(input=text, model=model).data
embeddings = get_embedding(dataframe["Description"].tolist())
# Einbettungsvektoren aus dem Einbettungsobjekt extrahieren
vectors = [embedding.embedding for embedding in embeddings]
array = np.array(vectors)
embeddings_series = pd.Series(list(array))
# Einbettungen als neue Spalte in das DataFrame hinzufügen.
dataframe['Description_Embeddings'] = embeddings_series

Nun fügen wir das neue DataFrame mit den Einbettungen in MyScale ein.

Boost Your AI App Efficiency now
Sign up for free to benefit from 150+ QPS with 5,000,000 vectors
Free Trial
Explore our product

# Verbindung mit MyScale herstellen

Wie wir zu Beginn besprochen haben, verwenden wir MyScale als Vektordatenbank zum Speichern und Verwalten von Daten. Hier stellen wir eine Verbindung zu MyScale her, um uns auf die Datenspeicherung vorzubereiten.

import clickhouse_connect
client = clickhouse_connect.get_client(
    host='Geben Sie_den_Host-Namen_hier_ein',
    port=443,
    username='Geben_Sie_den_Benutzernamen_hier_ein',
    password='Geben_Sie_das_Passwort_hier_ein'
)

Diese Verbindungseinrichtung stellt sicher, dass unsere Anwendung mit MyScale kommunizieren und die Leistungsfähigkeit von SQL für die Datenmanipulation und -analyse nutzen kann.

💡 Hinweis: Weitere Informationen zur Verbindung mit dem MyScale-Cluster finden Sie unter Verbindungsdetails (opens new window).

# Tabellen und Indizes mit MyScale erstellen

Wir erstellen nun eine Tabelle entsprechend unserem DataFrame. Alle Daten werden in dieser Tabelle gespeichert, einschließlich der Einbettungen.

client.command("""
    CREATE TABLE default.Events (
    Name String,
    Description String,
    Users_Going Int64,
    Users_Interested Int64,
    Users_Responded Int64,
    City String,
    Organized_By String,
    Street_Address String,
    Start_Date Date32,
    End_Date Nullable(Date32),
    Start_Time Nullable(DateTime64),
    Start_Day String,
    End_Day String,
    Description_Embeddings Array(Float32),
    CONSTRAINT check_data_length CHECK length(Description_Embeddings) = 1536
    ) ENGINE = MergeTree()
    ORDER BY (Name);
    """)

Die obigen SQL-Anweisungen erstellen eine Tabelle mit dem Namen Events auf dem Cluster. Die CONSTRAINT stellt sicher, dass alle Vektoreinbettungen die gleiche Länge von 1536 haben.

# Daten speichern und einen Index in MyScale erstellen

In diesem Schritt fügen wir die verarbeiteten Daten in MyScale ein. Dies beinhaltet das Stapel-Einfügen der Daten, um eine effiziente Speicherung und Abfrage zu gewährleisten.

batch_size = 10  # Je nach Bedarf anpassen

num_batches = len(dataframe) // batch_size

for i in range(num_batches):
    start_idx = i * batch_size
    end_idx = start_idx + batch_size
    batch_data = dataframe[start_idx:end_idx]
    # print(batch_data["Description_Embeddings"])
    client.insert("default.Events", batch_data.to_records(index=False).tolist(), column_names=batch_data.columns.tolist())
    print(f"Batch {i+1}/{num_batches} eingefügt.")

client.command("""
ALTER TABLE default.Events
    ADD VECTOR INDEX vector_index Description_Embeddings
    TYPE MSTG
""")

Mit Hilfe von pandas überträgt der obige Code unser vorbereitetes Dataset effizient in die MyScale-Datenbank.

Join Our Newsletter

# Datenanalyse mit MyScale

Schließlich nutzen wir die analytischen Fähigkeiten von MyScale, um Analysen durchzuführen und die semantische Suche zu ermöglichen. Durch die Ausführung von SQL-Abfragen können wir Veranstaltungen basierend auf Themen, Orten und Daten analysieren. Probieren wir also einige Abfragen aus.

# Einfache SQL-Abfrage

Lassen Sie uns zuerst die Top 10 Ergebnisse aus der Tabelle abrufen.

results=client.query("""
        SELECT Name,Description FROM default.Events LIMIT 10
    """)
for row in results.named_results():
        print(row["Name"])
        print(row['Description'])

Diese Abfrage gibt einfach die Top 10 Ergebnisse aus der Tabelle Events zurück.

# Veranstaltungen durch semantische Relevanz entdecken

Lassen Sie uns die Top 10 bevorstehenden Veranstaltungen mit einer ähnlichen Atmosphäre wie eine Referenzveranstaltung finden, z. B. "Eine der längsten laufenden Shows im Land - Betrieb seit 1974 ... JETZT unser 50. JAHR !!! Unser Schenectady". Dies wird erreicht, indem die semantischen Einbettungen der Veranstaltungsbeschreibungen verglichen werden, um Übereinstimmungen in Themen und Emotionen sicherzustellen.

embeddings=get_embedding(["Eine der längsten laufenden Shows im Land - Betrieb seit 1974 ... JETZT unser 50. JAHR !!! Unser Schenectady"])
embedding=embeddings[0].embedding
results = client.query(f"""
        SELECT Name, Description,
        distance(Description_Embeddings, {embedding}) as dist FROM default.Events ORDER BY dist LIMIT 10
    """)
for row in results.named_results():
        print("Titel der Veranstaltung  ", row["Name"])
        print("Beschreibung der Veranstaltung  ", row['Description'])
        print("Entfernung: ", row["dist"])

# Trendveranstaltungen nach Beliebtheit

Diese Abfrage bewertet die Top 10 Veranstaltungen nach der Anzahl der Teilnehmer und interessierten Benutzer, wobei beliebte Veranstaltungen von großen Stadtfestivals bis hin zu großen Konferenzen hervorgehoben werden. Sie ist ideal für diejenigen, die an großen, energiegeladenen Veranstaltungen teilnehmen möchten.

results = client.query(f"""
        SELECT Name, City, Users_Going, Users_Interested, Users_Responded
        FROM default.Events
        ORDER BY Users_Going DESC, Users_Interested DESC
        LIMIT 10
    """)
for row in results.named_results():
        print("Name der Veranstaltung  ", row["Name"])
        print("Stadt ", row["City"])
        print("Teilnehmer ", row["Users_Going"])
        print("Interessierte Benutzer ", row["Users_Interested"])

# Beliebte lokale Veranstaltungen in New York

Diese Abfrage kombiniert Relevanz und Beliebtheit und identifiziert ähnliche Veranstaltungen in New York City, die mit einer bestimmten Veranstaltung zusammenhängen, und sortiert sie nach Teilnehmerzahl. Sie bietet eine kuratierte Liste von Veranstaltungen, die die lebendige Kultur der Stadt widerspiegeln und lokales Interesse wecken.

embeddings=get_embedding(["Eine der längsten laufenden Shows im Land - Betrieb seit 1974 ... JETZT unser 50. JAHR !!! Unser Schenectady"])
embeddi=embeddings[0].embedding
results = client.query(f"""
        SELECT Name,City, Description, Users_Going,distance(Description_Embeddings, {embeddi}) as dist
        FROM default.Events
        WHERE City LIKE '%New York%' and dist < 1.5
        ORDER BY Users_Going DESC,dist
        LIMIT 10
    """)
for row in results.named_results():
        print("Name der Veranstaltung  ", row["Name"])
        print("Beschreibung ", row["Description"])
        print("Teilnehmer ", row["Users_Going"])

# Führende Veranstaltungsorganisatoren

Diese Abfrage bewertet die Top 10 Veranstaltungsorganisatoren nach der Gesamtzahl der Teilnehmer und interessierten Benutzer und hebt diejenigen hervor, die sich durch die Organisation von beeindruckenden Veranstaltungen und die Anziehung großer Publikumsmengen auszeichnen. Sie bietet Einblicke für Veranstaltungsplaner und Teilnehmer, die an erstklassigen Veranstaltungen interessiert sind.

# Welcher Kunde hat die meisten Benutzer angezogen
results = client.query(f"""
       SELECT Organized_By, SUM(Users_Going + Users_Interested) AS Total_Users
        FROM default.Events
        GROUP BY Organized_By
        ORDER BY Total_Users DESC
        Limit 10
    """)
for row in results.named_results():
        print("Name der Veranstaltung  ", row["Organized_By"])
        print("Gesamtbenutzer ", row["Total_Users"])

# RAG implementieren

Bisher haben wir MyScale für die Datenanalyse erkundet und seine Fähigkeiten zur Verbesserung unserer Datenworkflows hervorgehoben. Im nächsten Schritt gehen wir einen Schritt weiter und implementieren Retrieval-Augmented Generation (RAG), ein innovatives Framework, das eine externe Wissensbasis mit LLMs kombiniert. Dieser Schritt hilft Ihnen dabei, Ihre Daten besser zu verstehen und detailliertere Erkenntnisse zu gewinnen. Als Nächstes sehen Sie, wie Sie RAG mit MyScale verwenden können, um die Arbeit mit Daten interessanter und produktiver zu gestalten.

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate

query="Können Sie mir bitte einige Veranstaltungen im Zusammenhang mit Basketball vorschlagen"
# Verwenden Sie die oben definierte Methode get_embedding, sie akzeptiert eine Liste von Sätzen
embeddings=get_embedding([query])
embeddings=embeddings[0].embedding
results = client.query(f"""
        SELECT Name, City, Users_Going, Description, distance(Description_Embeddings, {embeddings}) as dist
        FROM default.Events
        ORDER BY Users_Going DESC,dist
        LIMIT 5
    """)
PROMPT_TEMPLATE = """
Ihr Ziel ist es, eine Antwort auf eine Frage nur mit den unten angegebenen Informationen zu formulieren:
{context}
---
Ihre Aufgabe besteht darin, den bereitgestellten Kontext sorgfältig zu analysieren und basierend ausschließlich auf den gegebenen Informationen eine Antwort auf die folgende Frage zu geben:
{question}
"""
# Kombinieren Sie die Beschreibungen der Top-Ergebnisse. 
descriptions = [row["Description"] for row in results.named_results()]
context_text = "\n\n---\n\n".join(descriptions)
prompt_template = ChatPromptTemplate.from_template(PROMPT_TEMPLATE)
prompt = prompt_template.format(context=context_text, question=query)
model = ChatOpenAI(openai_api_key="Ihr_API-Schlüssel_hier")
response_text = model.predict(prompt)
formatted_response = f"Antwort: {response_text}\n"
print(formatted_response)

Im Laufe dieses Blogs haben wir festgestellt, dass MyScale viel mehr ist als eine Vektordatenbank, die verwendet werden kann, um alle Arten von Anwendungen zu entwickeln. Wir können es als einfache SQL-Datenbank oder für fortgeschrittene KI-Anwendungen verwenden, die den Großteil des Entwicklungsbereichs abdecken. Wir ermutigen Sie, es zumindest auszuprobieren und die erweiterten Funktionen zu erkunden, indem Sie sich für die kostenlose Version anmelden und 5 Millionen kostenlose Vektorspeicher erhalten.

# Fazit

Wir haben die Fähigkeiten und Funktionen von MyScale mit Apify Scraper durch den Prozess der Entwicklung einer Event-Analyse-Anwendung erkundet. MyScale hat seine außergewöhnlichen Fähigkeiten in der leistungsstarken Vektorsuche unter Beweis gestellt und dabei alle Funktionen von SQL-Datenbanken beibehalten, was Entwicklern ermöglicht, semantische Suchen mit der vertrauten SQL-Syntax mit wesentlich besserer Geschwindigkeit und Genauigkeit durchzuführen.

Die Fähigkeiten von MyScale sind nicht auf diese Anwendung beschränkt. Sie können es verwenden, um jede KI-Anwendung mit der RAG (opens new window)-Methode zu entwickeln. MyScale hat andere Vektordatenbanken in Bezug auf Kosten, Geschwindigkeit und Genauigkeit übertroffen, also warum probieren Sie es nicht für Ihre nächste Anwendung aus?

Wenn Sie Feedback oder Vorschläge haben, kontaktieren Sie uns bitte über MyScale Discord (opens new window) oder Twitter (opens new window).

Keep Reading
images
RAG vs. Large Context LLMs: RAG wird bestehen bleiben

Die Iterationsgeschwindigkeit der generativen KI (GenAI) wächst exponentiell. Eine Konsequenz davon ist, dass das Kontextfenster - die Anzahl der Tokens, die ein großes Sprachmodell (LLM) gleichzeitig ...

Start building your Al projects with MyScale today

Free Trial
Contact Us