Verbesserung der Analytik mit Zeitreihen- und Vektordatenbanken
Die Zeitreihenanalyse spielt in vielen Bereichen eine entscheidende Rolle, insbesondere bei Internet-of-Things-Geräten (IoT). Mit Zeitreihendaten können wir Muster und Trends über bestimmte Zeiträume hinweg erkennen, wodurch wir zukünftige zeitabhängige Ereignisse prognostizieren und analysieren können. Häufige Beispiele für Anwendungsfälle von Zeitreihen sind die Vorhersage von Wettertemperaturen und Aktienkursen sowie die Überwachung von Sensordaten.
Als Zeitreihendatenbank bietet InfluxDB eine Lösung zum Speichern riesiger Mengen an Zeitreihendaten. InfluxDB ist durch Techniken wie Aggregationen und Downsampling stark für das Speichern und Abfragen zeitabhängiger Daten optimiert. Sich jedoch allein auf Zeitreihendatenbanken zu verlassen, kann eine Herausforderung sein, insbesondere wenn unser Anwendungsfall erfordert, dass wir eine Ähnlichkeitssuche durchführen.
In einem kürzlich gehaltenen Vortrag beim Zilliz Unstructured Data Meetup stellte Zoe Steinkamp, Developer Advocate bei InfluxDB, einen Ansatz vor, InfluxDB mit Milvus zu kombinieren, um zeitabhängige Anwendungsfälle zu speichern, abzufragen und Ähnlichkeitssuchen darauf durchzuführen.
In diesem Artikel werden wir dieses Thema ausführlicher untersuchen und Sie durch einen Anwendungsfall führen, bei dem wir Zeitreihendaten in InfluxDB speichern, die Daten abfragen, sie in Vektoreinbettungen umwandeln, die Einbettungen in Milvus speichern und schließlich eine Ähnlichkeitssuche mit Milvus durchführen. Also, ohne weitere Umschweife, legen wir los.
Zeitreihendaten verstehen
Zeitreihendaten stellen chronologisch geordnete Beobachtungen dar, die in bestimmten Intervallen aufgezeichnet werden, etwa stündlich, täglich, wöchentlich oder monatlich.
Wir finden Anwendungsfälle für Zeitreihen in unserem Alltag: von stündlichen Temperaturmessungen über tägliche Straßenverkehrsstatistiken bis hin zu monatlichen Einzelhandelsumsatzzahlen. Durch die Analyse von Zeitreihendaten können wir historische Muster aufdecken und diese Erkenntnisse nutzen, um zukünftige Entscheidungen zu unterstützen.
Bei der Analyse von Zeitreihendaten sehen wir häufig eines oder mehrere der folgenden Muster:
Saisonal: Wiederkehrende Schwankungen in festen Zeitintervallen, beeinflusst durch Faktoren wie Wochentage oder Monate.
Trend: Ein stetiger Anstieg oder Rückgang der Daten über einen längeren Zeitraum.
Zyklisch: Schwankungen, die saisonalen Mustern ähneln, jedoch ohne feste Frequenz.
Oben links – saisonal, oben rechts – abnehmender Trend, unten links – zunehmender Trend, unten rechts – zyklisch. Quelle.
Das Verständnis dieser Muster ist entscheidend für eine effektive Zeitreihenprognose. Die Zeitreihenprognose ist eine leistungsstarke analytische Technik, die zukünftige Werte zeitabhängiger Variablen auf Grundlage historischer Daten vorhersagt. Wenn wir zum Beispiel die Kurshistorie einer Aktie für die letzten zwei Jahre haben und den Aktienkurs von morgen vorhersagen möchten, führen wir im Wesentlichen eine Zeitreihenprognose durch.
Unternehmen aus verschiedenen Sektoren haben das Konzept der Zeitreihenprognose in ihrem täglichen Betrieb für verschiedene Zwecke angewendet:
Finanzinstitute: Vorhersage von Aktienkursen, Prognose von Währungsschwankungen, Erkennung ungewöhnlicher Muster im Ausgabeverhalten von Kunden oder Entwicklung robuster Risikomanagementstrategien.
Gesundheitssektor: Überwachung der Ausbreitung von Krankheiten, Echtzeitverfolgung der Vitalzeichen von Patienten oder Verbesserung der allgemeinen Patientenversorgung und -ergebnisse.
Einzelhandel: Prognose von Verkaufsvolumina, Verständnis des Kaufverhaltens von Kunden, Optimierung der Bestandsverwaltung, Feinabstimmung von Preisstrategien und Steigerung der allgemeinen Rentabilität.
Internet of Things (IoT): Smart-Home-Systeme verwenden Sensordaten, um Aufgaben zu automatisieren, während Thermostate und Home Hubs Informationen austauschen, um den Energieverbrauch zu optimieren.
Fertigung: Reduzierung von Maschinenausfallzeiten, Implementierung vorausschauender Wartungsstrategien und Verbesserung der allgemeinen betrieblichen Effizienz.
Aufgrund ihrer zeitabhängigen Natur werden Zeitreihendaten typischerweise kontinuierlich in Echtzeit erfasst. Infolgedessen kann das Volumen von Zeitreihendaten schnell wachsen, wodurch eine skalierbare und effiziente Lösung für die Speicherung erforderlich wird. Hier kommen Zeitreihendatenbanken ins Spiel.
InfluxDB als Zeitreihendatenbank
Zeitreihendatenbanken müssen im Vergleich zu traditionellen relationalen Datenbanken mehrere Schlüsseleigenschaften besitzen, um den Speicher- und Abrufprozess großer Mengen von Zeitreihendaten zu erleichtern. Wie bereits erwähnt, werden Zeitreihendaten häufig in Echtzeit erfasst; daher müssen die Daten verarbeitet und über alle Systeme hinweg synchronisiert werden, sobald sie die Datenbank erreichen.
Zeitreihendatenbanken bieten eine bessere Leistung und Effizienz als relationale Datenbanken für zeitbasierte Workloads, insbesondere beim Umgang mit großen Datenmengen und hochfrequenten Schreibvorgängen. Darüber hinaus bietet eine Zeitreihendatenbank hochoptimierte Lösungen für Vorgänge wie Datenerfassung, Abfragen und Abruf über Zeitbereiche hinweg sowie zeitbasierte Analysen und Aggregationen.
Wichtige Funktionen in einer Zeitreihendatenbank
InfluxDB ist eine der Open-Source-Zeitreihendatenbanken, die wir zur Speicherung von Zeitreihendaten verwenden können. Sie ist in Rust geschrieben und stark für Extract-, Transform-, Load-(ETL)-Vorgänge in Datenbanken optimiert. Um eine schnelle Datenerfassung und Zeitstempelsynchronisierung zu ermöglichen, enthält InfluxDB einen integrierten Zeitdienst, der das Network Time Protocol (NTP) verwendet.
Die Zeitreihendaten, die wir in einer Zeitreihendatenbank speichern, bestehen typischerweise nur aus drei Spalten: einem Tag, einem Feld und einem Zeitstempel. Ein Tag enthält Metadaten der Daten, die wir messen, ein Feld enthält Werte, die wir weiter analysieren, und ein Zeitstempel gibt an, wann die Daten erfasst wurden.
Nehmen wir an, wir speichern alle 5 Minuten Sensormessdaten. Die Daten in unserer Zeitreihe würden etwa wie die folgende Visualisierung aussehen:
Beispiel für in einer Zeitreihendatenbank gespeicherte Daten
Nachdem wir nun die Grundlagen von Zeitreihendatenbanken kennen, sprechen wir über Vektordatenbanken!
Milvus als Vektordatenbank
Wie der Name schon sagt, speichert eine Vektordatenbank Daten (einschließlich Bilder, Texte, Dokumente usw.) in Vektoren. Wir wandeln unsere Daten mithilfe eines der vielen Deep-Learning-Modelle in Vektoren um.
Die Dimension eines Vektors hängt von der Technik oder dem Deep-Learning-Modell ab, das wir zu seiner Erzeugung verwenden. Wenn wir beispielsweise ein Modell namens all-MiniLM-L6-v2 verwenden, erhalten wir einen Vektor der Dimension 384. Wenn wir hingegen ein Modell namens all-mpnet-base-v2 verwenden, erhalten wir einen Vektor der Dimension 768.
Wie Ihnen vielleicht bekannt ist, ist ein Vektor nicht nur ein mathematisches Objekt; er umfasst sowohl Betrag als auch Richtung und trägt die semantische Bedeutung des Textes, Bildes oder Dokuments, das er repräsentiert. Dieser tiefgreifende Aspekt von Vektoren ermöglicht es, sie im Vektorraum nahe beieinander zu platzieren, wodurch ähnliche Texte oder Bilder repräsentiert werden.
Semantische Ähnlichkeit zwischen Vektoren in einem Vektorraum
Die Vorstellung, dass ein Vektor die semantische Bedeutung von Daten trägt, ermöglicht es uns, die Ähnlichkeit eines beliebigen Vektorpaares über Metriken wie Kosinus-Ähnlichkeit oder euklidische Distanz zu vergleichen. Wenn die Kosinus-Ähnlichkeit zwischen zwei Vektoren nahe bei eins liegt, dann sind die beiden Vektoren sehr ähnlich, und umgekehrt.
Das Speichern und Durchführen von Ähnlichkeitssuchen kann einfach zu implementieren sein, wenn wir nur wenige Vektoren haben. In realen Anwendungen haben wir es jedoch typischerweise mit Hunderttausenden oder Millionen von Vektoren zu tun, wodurch der gesamte Vorgang sowohl in Bezug auf Zeit als auch auf Rechenressourcen aufwendiger zu warten ist. An dieser Stelle wird eine Vektordatenbank notwendig.
Vektordatenbanken wie Milvus und Zilliz Cloud bieten hochgradig skalierbare und effiziente Prozesse zum Speichern von Millionen von Vektoren. Dank ihrer großen Vielfalt an fortschrittlichen Indexierungsmethoden wie IVF-Flat, HNSW und anderen sind sie außerdem stark für schnellere Vektorähnlichkeitssuchen und Datenabfragen optimiert.
Vollständiger Workflow eines Vektorsuchvorgangs
Der Workflow zum Speichern und Abrufen von Daten aus Milvus oder einer anderen Vektordatenbank sieht wie folgt aus: Zuerst transformieren wir unsere Eingabedaten, bei denen es sich um Texte oder Bilder handeln kann, mithilfe einer Technik oder eines Modells unserer Wahl in Vektoren. Anschließend nehmen wir diese Vektoren zusammen mit ihren Metadaten in die Vektordatenbank auf und erstellen mithilfe einer Indexierungsmethode unserer Wahl einen Index.
Während des Abrufprozesses müssen wir unsere Abfrage zunächst mithilfe derselben Technik oder desselben Modells, das wir während des Aufnahmeprozesses verwendet haben, in einen Vektor umwandeln. Anschließend führen wir einen Vektorsuchvorgang durch, um diesen Abfragevektor mit den in unserer Vektordatenbank gespeicherten Vektoren zu vergleichen. Schließlich werden uns die ähnlichsten Vektoren in der Datenbank als Ergebnisse zurückgegeben.
Der Anwendungsfall der Kombination von Zeitreihendatenbank und Vektordatenbank
Da Zeitreihen- und Vektordatenbanken für unterschiedliche Anwendungsfälle hochgradig optimiert sind, können wir die Stärken beider Datenbanken in unseren realen Projekten nutzen.
Stellen wir uns zum Beispiel ein Szenario vor, in dem wir ein System zur Analyse von Echtzeit-Verkehrsbedingungen in einer Smart City entwickeln. Um dies zu erreichen, würden wir Sensoren an vielen Standorten installieren, die die Fahrzeuggeschwindigkeit, die Fahrzeuganzahl und andere relevante Metriken erfassen können. Die durchschnittliche Fahrzeuggeschwindigkeit und die Fahrzeuganzahl über bestimmte Zeitintervalle könnten kontinuierlich in einer Zeitreihendatenbank gespeichert werden.
Zusätzlich zu Sensoren könnten wir auch Kameras installieren, um Fotos oder Videos der tatsächlichen Verkehrsbedingungen aufzunehmen. In diesem Fall können wir die Fotos und Videos mithilfe eines Deep-Learning-Modells unserer Wahl in Vektoren umwandeln und diese Vektoren in einer Vektordatenbank speichern. Durch die Kombination der Zeitreihendaten aus der Zeitreihendatenbank und der Vektoren aus der Vektordatenbank können wir eine Anomalieerkennung für Verkehrsbedingungen durchführen.
Beispielanwendungsfall für die Kombination von InfluxDB und Milvus zur Analyse von Verkehrsbedingungen
In diesem Abschnitt werden wir mithilfe von InfluxDB und Milvus einen einfachen Anwendungsfall im Zusammenhang mit Verkehrsbedingungen implementieren. Konkret werden wir Zeitreihendaten in InfluxDB und die entsprechenden Vektoren in Milvus speichern.
Lassen Sie uns zunächst Dummy-Zeitreihendaten generieren, die aus Zeitstempel, durchschnittlicher Fahrzeuggeschwindigkeit, Fahrzeuganzahl und Anomalietyp bestehen. Der Anomalietyp besteht aus zwei unterschiedlichen Werten: "normal" und "accident".
Wir beginnen damit, 500 "normale" Datenpunkte zu generieren, die alle 10 Minuten erfasst werden.
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
import random
def generate_sensor_data(anomaly_type, start_time, vehicle_count_range, avg_speed_range, rows=500, seed = 42):
np.random.seed(seed)
vehicle_counts = np.random.randint(vehicle_count_range[0], vehicle_count_range[1], size=rows)
avg_speeds = np.random.uniform(avg_speed_range[0], avg_speed_range[1], size=rows)
start_time = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
timestamps = [start_time + timedelta(minutes=10*i) for i in range(rows)]
df = pd.DataFrame({
'Timestamp': timestamps,
'Vehicle Count': vehicle_counts,
'Average Speed': avg_speeds,
'Anomaly Type': anomaly_type
})
return df
vehicle_count_range = (5, 10)
avg_speed_range = (60.0, 80.0)
df_normal = generate_sensor_data("normal", "2024-09-15 18:00:00", vehicle_count_range, avg_speed_range)
DataFrame normal
Als Nächstes generieren wir 500 "accident"-Datenpunkte, die ebenfalls alle 10 Minuten erfasst werden. Wie Sie sich vielleicht vorstellen können, sind zwei mögliche Anzeichen für einen Unfall auf den Straßen eine höhere Fahrzeuganzahl und eine niedrigere Durchschnittsgeschwindigkeit aufgrund von Verkehrsstaus.
vehicle_count_range = (20, 40)
avg_speed_range = (10.0, 20.0)
df_accident = generate_sensor_data("accident", "2024-09-19 05:20:00", vehicle_count_range, avg_speed_range)
DataFrame accident
Jetzt verketten wir die beiden Dataframes und fügen dann die verketteten Daten mit einem Befehl, der wie folgt aussieht, in InfluxDB ein:
from influxdb_client_3 import InfluxDBClient3
df = pd.concat([df_accident, df_normal], axis=0)
client = InfluxDBClient3(token="DATABASE_TOKEN",
host="HOST",
database="DATABASE_NAME")
client.write(bucket="DATABASE_NAME", record=df, data_frame_measurement_name='traffic_data', data_frame_tag_columns=['Anomaly Type'], data_frame_timestamp_column='Timestamp')
Und Sie können mit dem folgenden Befehl eine Abfrage der Daten in InfluxDB durchführen.
query = "SELECT * FROM traffic_data WHERE time >= now() - INTERVAL '90 days'"
pd = client.query(query=query, mode="pandas")
Wenn Sie mehr über verschiedene Operationen im Detail erfahren möchten, lesen Sie bitte diese InfluxDB Python Client Library-Dokumentation.
Jetzt generieren wir einige Vektordaten. Wie oben dargestellt, können wir Fotos oder Videos von Verkehrsbedingungen zu bestimmten Zeitintervallen als Datenquellen verwenden und sie mithilfe eines Deep-Learning-Modells in Vektoren umwandeln. Da wir jedoch keine Fotos oder Videos haben, erfassen wir die durchschnittliche Fahrzeuggeschwindigkeit über bestimmte Zeitintervalle in einer Liste und wandeln sie im Wesentlichen in Vektoren um. In der Zeitreihenanalyse wird diese Methode Windowing genannt.
Im folgenden Beispiel setzen wir die Fenstergröße auf 24, aber Sie können diese auf jede beliebige Größe anpassen. Das bedeutet, dass wir für jede Beobachtung die Werte der durchschnittlichen Fahrzeuggeschwindigkeit für die nächsten 240 Minuten erfassen, da jeder Datenpunkt alle 10 Minuten erfasst wird. Als Ergebnis erhalten wir für jede Beobachtung einen 24-dimensionalen Vektor.
Für jede Beobachtung verwenden wir eine Schrittgröße von 10. Das bedeutet, dass sich die Startzeit zwischen aufeinanderfolgenden Beobachtungen um 100 Minuten unterscheidet. Zusätzlich müssen wir für jedes Vektorelement jeder Beobachtung eine Normalisierung durchführen, um sicherzustellen, dass der Vektor einheitlich von 0 bis 1 skaliert ist.
window_size = 24
step_size = 10
min_val = 10 # Min average vehicle speed
max_val = 80 # Max average vehicle speed
def vectorize_data(df):
windows = [
df.iloc[i : i + window_size]
for i in range(0, len(df) - window_size + 1, step_size)
]
start_times = [w["Timestamp"].iloc[0] for w in windows]
end_times = [w["Timestamp"].iloc[-1] for w in windows]
avg_speed_values = [w["Average Speed"].tolist() for w in windows]
anomaly_types = [w["Anomaly Type"].tolist()[0] for w in windows]
# Create a new DataFrame from the collected data
embedding_df = pd.DataFrame(
{"start_time": start_times, "end_time": end_times, "vectors": avg_speed_values, "anomaly_types": anomaly_types}
)
embedding_df["vectors"] = embedding_df["vectors"].apply(normalize_vector)
# Apply a lambda function to convert timestamps to Unix timestamp format.
embedding_df['start_time'] = embedding_df['start_time'].apply(lambda x: pd.Timestamp(x).timestamp()).astype(int)
embedding_df['end_time'] = embedding_df['end_time'].apply(lambda x: pd.Timestamp(x).timestamp()).astype(int)
return embedding_df
# Function to normalize the sensor column
def normalize_vector(vectors: list) -> list:
return (
[0.0] * len(vectors)
if max_val == min_val
else [(v - min_val) / (max_val - min_val) for v in vectors]
)
embedding_df = vectorize_data(df)
DataFrame-Vektoren
Wie Sie oben sehen können, haben wir jetzt einen Dataframe mit Startzeit, Endzeit, Vektor und Anomalietyp. Als Nächstes können wir diese Daten direkt in die Milvus-Datenbank aufnehmen. Der einfachste Weg, mit Milvus zu beginnen, ist über Milvus Lite. Installieren wir also zuerst Milvus Lite und erstellen dann ein Schema entsprechend den Spalten in unserem Dataframe.
! pip install pymilvus==2.4.6
from pymilvus import MilvusClient, DataType
dim = 24
collection_name = "traffic_data"
milvus_client = MilvusClient("./local_test.db")
has_collection = milvus_client.has_collection(collection_name, timeout=5)
if has_collection:
milvus_client.drop_collection(collection_name)
schema = milvus_client.create_schema(enable_dynamic_field=True)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("start_time", DataType.INT64)
schema.add_field("end_time", DataType.INT64)
schema.add_field("vector", DataType.FLOAT_VECTOR, dim=dim)
schema.add_field("anomaly_type", DataType.VARCHAR, max_length=64)
index_params = milvus_client.prepare_index_params()
index_params.add_index(field_name = "vector", metric_type="L2")
milvus_client.create_collection(collection_name, schema=schema, index_params=index_params, consistency_level="Strong")
Nachdem wir nun ein Schema wie oben definiert erstellt haben, können wir unseren Dataframe mit dem folgenden Befehl in Milvus aufnehmen:
data = [
{
"id": i,
"vector": embedding_df['vectors'].iloc[i],
"start_time": embedding_df['start_time'].iloc[i],
"end_time": embedding_df['end_time'].iloc[i],
"anomaly_type": embedding_df['anomaly_types'].iloc[i]
}
for i in range(len(embedding_df))
]
insert_result = milvus_client.insert(collection_name, data)
Und das war’s! Jetzt können wir eine Vektorsuchoperation durchführen.
Stellen wir uns vor, dass wir eine Woche später einen ungewöhnlichen Rückgang der durchschnittlichen Fahrzeuggeschwindigkeit in einem bestimmten Gebiet beobachten. Im folgenden Beispiel generieren wir diese Daten manuell mithilfe von Funktionen, die wir zuvor definiert haben. In einem realen Szenario würden Sie diese Daten jedoch höchstwahrscheinlich anhand des Zeitstempels aus InfluxDB lesen und abfragen und den resultierenden Zeitstempel verwenden, um den entsprechenden Vektor in Milvus zu finden.
vehicle_count_range = (20, 40)
avg_speed_range = (10.0, 20.0)
df_test = generate_sensor_data("accident", "2024-09-26 12:00:00", vehicle_count_range, avg_speed_range, rows=30, seed = 1)
Als Nächstes transformieren wir diese Daten mit denselben Fensterungs- und Normalisierungsmethoden wie zuvor.
query_vector = vectorize_data(df_test).vectors.values
Und jetzt können wir mit dem folgenden Befehl eine Vektorsuchoperation auf dem Abfragevektor durchführen:
result = milvus_client.search(collection_name, query_vector, limit=3, output_fields=["vector", "anomaly_type"])
for hits in result:
for hit in hits:
print(f"hit: {hit}")
"""
Output:
hit: {'id': 21, 'distance': 0.04718003422021866, 'entity': {'anomaly_type': 'accident'}}
hit: {'id': 41, 'distance': 0.0641128420829773, 'entity': {'anomaly_type': 'accident'}}
hit: {'id': 34, 'distance': 0.06488440930843353, 'entity': {'anomaly_type': 'accident'}}
"""
Wie Sie sehen können, haben die drei ähnlichsten Beobachtungen zu unserer Abfrage alle den Anomalietyp „accident“, was die niedrige Durchschnittsgeschwindigkeit bestätigt, die in unseren Abfragedaten aufgezeichnet wurde. Wenn Sie die Vektorvisualisierung zwischen den Abfragedaten und den drei ähnlichsten Beobachtungen untersuchen, werden Sie feststellen, dass der Bereich der Vektorwerte einander ähnelt.
Vergleich zwischen Abfragevektor und den drei ähnlichsten Vektoren
Fazit
In diesem Artikel haben wir die Kombination von Zeitreihendatenbanken und Vektordatenbanken untersucht, mit Schwerpunkt auf dem Anwendungsfall in Internet-of-Things-(IoT)-Anwendungen. Zeitreihendatenbanken wie InfluxDB sind hocheffizient beim Speichern und Abfragen chronologischer Daten, was für Anwendungen wie Wettervorhersage, Börsenanalyse und Sensorüberwachung entscheidend ist. Allerdings stoßen sie an Grenzen, wenn es um die Durchführung von Ähnlichkeitssuchen geht.
Um dies zu überwinden, haben wir Zeitreihendatenbanken mit Vektordatenbanken wie Milvus kombiniert, die Daten in Vektorform speichern und so effiziente Ähnlichkeitssuchen mithilfe von Techniken wie Kosinusähnlichkeit oder euklidischer Distanz ermöglichen. Durch die Kombination der beiden Datenbanken können die Stärken beider Systeme vollständig genutzt werden. Wie Sie im obigen Beispiel sehen können, können Zeitreihendaten von Sensoren in InfluxDB gespeichert werden, während Vektordaten in Milvus gespeichert werden können. Diese Integration ermöglicht fortschrittliche Anwendungsfälle wie die Anomalieerkennung bei Verkehrsbedingungen in Echtzeit.
Der in diesem Artikel vorgestellte Code im Zusammenhang mit Milvus kann über dieses Colab-Notebook aufgerufen werden.
Sie können sich auch diesen Artikel ansehen, der Vorverarbeitungsmethoden untersucht, um Zeitreihendaten in ein für Prognoseaufgaben geeignetes Embedding umzuwandeln.
Weiterlesen

A Few Notes from Databricks Data + AI Summit 2026: Why the Data Layer Matters Again
James Luan shares notes from Databricks Data + AI Summit 2026 on why production AI is pushing the data layer back to the center of infrastructure.

The AWS Outage Was a Wake-Up Call for Vector Database Cross-Region Disaster Recovery
Zilliz Cloud Had the Answer Before the Crisis. Zilliz Cloud is the world's first vector database with native cross-region disaster recovery.

Introducing DeepSearcher: A Local Open Source Deep Research
In contrast to OpenAI’s Deep Research, this example ran locally, using only open-source models and tools like Milvus and LangChain.


