Clean Architecture、サブプロセススクレイパー、SQLite FTS5、ローカルLLM(Ollama)をDocker化されたPython + Reactスタック内に統合する手法。


1. エグゼクティブサマリーと設計目標

BMW AutoTrend Dashboard は、ローカルファーストでAIを活用した自動車向けインテリジェンスプラットフォームです。BMW関連ニュースの市場動向やセンチメントを収集・処理・分類・可視化します。システムは完全にローカルで動作するよう設計されており、記事の要約やエンティティ抽出には Ollama によるローカルLLMインスタンスを使用し、確定的なルールベースの正規表現フォールバックエンジンでバックアップします。

プラットフォームは以下の設計目標に基づいて構築されています:

  1. Clean Architecture:データ抽出レイヤー、ストレージメカニズム、分析計算、APIコントローラを分離する。
  2. 確定的なフォールバック:ローカルLLMリソース(Ollama)がオフラインまたはリソース制約下にある場合でも、ルールベースの正規表現プロセッサを使用してアプリケーションが完全に機能するようにする。
  3. ローカルファーストと高パフォーマンス:高速全文検索に SQLite FTS5 を使用し、スクレイピングしたデータ、検索インデックス、分析ファイルを外部SaaS APIに依存せずにローカルに保存する。
  4. プラグアンドプレイの拡張性:統一された抽象インターフェースを実装することで、新しいスクレイピングアダプター(例: Autoblog、MotorTrend)を追加可能にする。

2. Clean System Architecture

アプリケーションは Clean Architecture の原則に従い、厳格な単方向データフローとビジネスロジックの外部依存からの分離を維持します。

graph TD
    subgraph Frontend [React SPA - Served via Nginx]
        UI[Interactive Dashboard Pages]
        RC[Recharts Visualizations]
        FTS_UI[Spotlight Search UI]
    end

    subgraph Backend [FastAPI Application]
        API[FastAPI Endpoints]
        SCH[APScheduler Ingestion & Snapshots]
        ING[Ingestion Pipeline]
        ANA[Analytics Engine]
        AIP[AI Processing Pipeline]
    end

    subgraph CLI Bridge [Subprocess Execution]
        WBC[Webcmd CLI + BMWBLOG Plugin]
    end

    subgraph Storage [Local Storage]
        DB[(SQLite Database)]
        FTS[(FTS5 Search Index)]
    end

    subgraph Local LLM [AI Inference]
        OLL[Ollama Server]
    end

    UI -->|Queries| API
    API -->|Reads/Writes| DB
    ING -->|Executes| WBC
    WBC -->|Scrapes Web Data| BMWBLOG[BMWBLOG Site]
    ING -->|Sends content for classification| AIP
    AIP -->|Requests JSON| OLL
    AIP -->|Regex Fallback| AIP
    SCH -->|Triggers| ING
    SCH -->|Runs daily| ANA
    ANA -->|Computes stats| DB
    FTS_UI -->|Queries FTS| API
    API -->|FTS Match Query| FTS

Enter fullscreen mode Exit fullscreen mode

  • フロントエンドレイヤー:Viteで構築されたReact + TypeScript SPA。バックエンドとは標準化されたJSON RESTエンドポイントを介してのみやり取りする。
  • APIコントローラー:Pydanticを使用してスキーマを検証し、ストレージにクエリを実行し、バックグラウンドタスクをトリガーするFastAPIハンドラー。
  • コアビジネスロジック
    • Ingestion Pipeline はスクレイピング、重複チェック、データ取り込みを統括する。
    • AI Processing Pipeline はテキスト分析、要約、タグ抽出を実行する。
    • Analytics Engine はメトリクスを集計し、日次スナップショットを維持する。
  • データプロバイダー:Node.jsベースのCLIツール(webcmd)の実行をカプセル化するサブプロセスラッパー。
  • データベースレイヤー:SQLAlchemy ORM経由で管理されるSQLiteに生SQLデータベーストリガーを追加。

3. インジェスチョンパイプラインとサブプロセスWebCMDスクレイピング

データインジェスチョンは、[providers/base.py]の抽象基底クラス NewsProvider から始まります。providers/bmwblog.pyBMWBlogProvider のような具象プロバイダーは、出版社からデータをクロールする役割を担います。

脆弱で保守しにくいカスタムウェブスクレイパーを書く代わりに、バックエンドは内部で @agentrhq/webcmd CLIツールを利用します。BMWBlogProvider は構造化された記事フィードを取得するために webcmd をサブプロセスとして実行します。

サブプロセス実行メカニズム

# snippet from backend/providers/bmwblog.py
def _run_webcmd(self, args: List[str]) -> str:
    cmd = ["webcmd"] + args
    try:
        # We use shell=True on Windows because webcmd is a script (.ps1 or .cmd)
        is_windows = os.name == 'nt'
        result = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            shell=is_windows,
            check=True
        )
        return result.stdout
    except subprocess.CalledProcessError as e:
        logger.error(f"webcmd execution failed: {result.stderr}")
        raise Exception(f"webcmd error: {result.stderr}")

Enter fullscreen mode Exit fullscreen mode

パイプラインは2段階で実行されます:

  1. メタデータ取得:最新10件の記事(メタデータのみ:URL、タイトル、簡単な抜粋)を取得するために webcmd bmwblog latest -f json を実行する。
  2. 詳細記事の取り込み:SQLiteインデックスで確認した新しいURLごとに、webcmd bmwblog article <url> -f json をクエリして本文全文、カテゴリ、著者を抽出し、OpenGraph画像(og:image)の生HTMLをスクレイピングする。

4. ハイブリッドAI分類パイプライン(Ollamaと正規表現フォールバック)

全文記事テキストが取り込まれると、ai/processor.py にディスパッチされます。プロセッサはOllamaの可用性を動的にチェックして評価エンジンを選択するハイブリッドシステムを使用します:

graph TD
    A[New Article Ingested] --> B{Is Ollama Server Online?}
    B -->|Yes| C[Call Ollama Llama 3.2 API]
    C --> D{Parsing JSON Successful?}
    D -->|Yes| E[Save AI Classification to Database]
    D -->|No| F[Fallback to Regex Rule Engine]
    B -->|No| F
    F --> G[Extract Entities, Sentiment, & Summaries via Rules]
    G --> E

Enter fullscreen mode Exit fullscreen mode

1. Ollama LLMパイプライン

Ollamaサーバーがオンラインで llama3.2 を実行している場合、バックエンドは慎重に作成したプロンプトとともに /api/generate にPOSTします。確定的な統合を保証するため、リクエストは構造化されたJSON出力を強制します:

payload = {
    "model": settings.OLLAMA_MODEL,
    "prompt": prompt,
    "format": "json",
    "stream": False,
    "options": { "temperature": 0.1 }
}

Enter fullscreen mode Exit fullscreen mode

モデルはセンチメント(Positive、Neutral、Negative)を分類し、関連する車両モデル(例: Neue KlasseBMW M3)を抽出し、技術タグ(例: Battery TechnologyADAS)をマッピングし、短く詳細なTL;DR要約を生成します。

2. ルールベース正規表現フォールバックパイプライン

Ollamaがオフライン(または有効なJSONを返せない)場合、_analyze_with_rules が実行されます。事前コンパイルされた正規表現を使用してキーワードやタグをマッチングします:

TECHNOLOGY_TAGS = {
    "Electric Vehicles": r"\b(ev|evs|electric|zero-emission|zero emission|battery electric|bev)\b",
    "Battery Technology": r"\b(battery|batteries|solid-state|cell|cells|rimac)\b",
    "Autonomous Driving": r"\b(autonomous|self-driving|driverless|autopilot)\b",
    # ...
}

# Sentiment heuristic based on term tallying
pos_count = sum(len(re.findall(rf"\b{word}\b", combined_text)) for word in SENTIMENT_POSITIVE)
neg_count = sum(len(re.findall(rf"\b{word}\b", combined_text)) for word in SENTIMENT_NEGATIVE)

Enter fullscreen mode Exit fullscreen mode

このハイブリッドモデルにより、データベースカラムに有効なカテゴリとタグが確実に設定され、ハードウェアの制約によって検索機能が損なわれることはありません。


5. データベーストリガーを用いた高性能SQLite FTS5検索インデックス

高速でローカルファーストな検索を実現するため、プロジェクトは遅い LIKE %query% SQL操作を回避し、SQLiteネイティブの FTS5 (Full-Text Search) 拡張機能を利用します。

データベーススキーマの初期化とトリガー

database/connection.py では、SQLAlchemyを使用してデータベースを起動しますが、標準的な接続イベントにフックして外部キー制約(PRAGMA foreign_keys=ON)を強制し、手動でFTS5仮想テーブルと同期トリガーをプロビジョニングします:

-- FTS5 Virtual Table Configuration
CREATE VIRTUAL TABLE articles_fts USING fts5(
    title, 
    excerpt, 
    content, 
    content='articles'
);

-- Synchronization Triggers (Insert, Delete, Update)
CREATE TRIGGER articles_ai AFTER INSERT ON articles BEGIN
    INSERT INTO articles_fts(rowid, title, excerpt, content)
    VALUES (new.id, new.title, new.excerpt, new.content);
END;

Enter fullscreen mode Exit fullscreen mode

これらのトリガーは検索インデックス作成をSQLiteエンジンに直接オフロードします。SQLAlchemy経由で新しい記事がコミットされると、SQLiteは自動的にFTS5シャドウテーブル内のタイトル、抜粋、本文をインデックス化します。

関連性ベースの検索エンドポイント

ユーザーがダッシュボードで検索すると、バックエンドはBM25関連性ランキングクエリを実行します:

# Snippet from backend/api/routes.py
query_str = """
    SELECT rowid FROM articles_fts 
    WHERE articles_fts MATCH :query
    ORDER BY bm25(articles_fts)
"""
result = db.execute(text(query_str), {"query": search_term})

Enter fullscreen mode Exit fullscreen mode

これによりサブミリ秒で実行され、フロントエンドに即時の「Search-as-you-type」機能を提供します。


6. 動的アナリティクスエンジンとローリングトレンド計算

analytics/engine.pyAnalytics Engine は、どの自動車トレンドが勢いを増しているかを判断するためのローリングウィンドウ計算を実行します。

基本的なカウントを追跡する代わりに、エンジンは過去7日間とその前の7日間の車両モデルおよび技術の言及を比較して 週次成長率 を計算します。

# 7-day boundaries
seven_days_ago = today_start - timedelta(days=7)
fourteen_days_ago = today_start - timedelta(days=14)

# Mentions in last 7 days (recent) vs 7-14 days ago (previous)
# Growth calculation:
if prev_cnt == 0:
    growth_pct = 100.0 if recent_cnt > 0 else 0.0
else:
    growth_pct = ((recent_cnt - prev_cnt) / prev_cnt) * 100.0

Enter fullscreen mode Exit fullscreen mode

結果のトピックは「Trending Topics」ウィジェットに表示するため growth_rate DESC でソートされます。

履歴スナップショット

ユーザーがページを読み込むたびにCPU負荷の高い集計を実行しないよう、アプリケーションはAPSchedulerを使用して深夜近くに generate_daily_snapshot を実行します。この関数は計算された統計をJSONにシリアライズし、analytics_snapshots テーブルに保存して、30日間の履歴タイムラインを高速に描画できるようにします。


7. フロントエンドアーキテクチャ(React + Vite + Recharts)

フロントエンドは、最新のReact + TypeScript + Viteアーキテクチャで構築されたシングルページアプリケーションです。

UI機能

  • Glassmorphicテーマ:CSS変数、カスタムOutfitタイポグラフィ、発光ボーダーを用いたプレミアムダークモードインターフェース。
  • コンポーネントレイアウト:サイドバーとメインパネルはレスポンシブナビゲーション用に layouts/DashboardLayout.tsx で構成。
  • ビジュアライゼーションRecharts を使用して動的データを表示:
    • 日次記事量のエリアチャート。
    • 30日間のPositive/Neutral/Negativeトレンドを示すセンチメントタイムライン。
    • 車両モデルおよび技術タグの人気を比較する棒グラフ。
  • リアルタイム検索:キー入力を取り込み、API経由でSQLite FTS5インデックスをクエリし、サブミリ秒の遅延で一致部分をハイライト。

8. DockerオーケストレーションとDevSecOpsの知見

プラットフォームは単一コマンド docker compose up --build で起動するよう設計されています。オーケストレーションは3つのネットワーク分離された依存コンテナを設定します:

  1. Ollamaコンテナ:ポート 11434 でLLMモデルを取得・提供。
  2. FastAPIバックエンドコンテナdocker/backend.Dockerfile からビルド。データベースを永続的なDockerボリューム db_data/app/data/autotrend.db としてマッピングし、コンテナ再起動後もデータベースレコードを永続化。
  3. Nginxフロントエンドコンテナ:Reactアプリケーションをコンパイルし、Nginxで静的アセットをポート 80 で提供し、バックエンドへのリクエストをプロキシするマルチステージビルド。

Docker技術Tips:Node.jsサブプロセス要件

Pythonバックエンドが webcmd(npmライブラリ)をサブプロセスとして呼び出すため、バックエンドDockerfileはマルチランタイム環境をサポートするよう構成する必要があります。Python slimコンテナ内にNode.js、npm を追加し、@agentrhq/webcmd をグローバルインストールすることで、インジェスチョンパイプラインがエラーなく動作します:

FROM python:3.12-slim
WORKDIR /app

# Install Node.js, NPM, and global webcmd CLI scraper
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential curl gnupg \
    && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && npm install -g @agentrhq/webcmd \
    && webcmd plugin install github:agentrhq/webcmd/bmwblog \
    && rm -rf /var/lib/apt/lists/*

Enter fullscreen mode Exit fullscreen mode


9. 拡張性と将来の展望

AutoTrend Dashboardの設計は容易に拡張可能です:

  • 新しいニュース媒体の追加NewsProvider 抽象基底クラスを実装する新しいクラス(例: AutoblogProvider)を作成。webcmd autoblog latest のようなスクレイパープラグインコマンドをマッピングし、run_ingestion_pipeline にプロバイダーを登録。
  • 外部LLM:[ai/client.py] のAIクライアントは、環境変数でAPIキーを指定することでリモートクラウドモデル(Gemini APIやOpenAI APIなど)に対応可能。
  • 高度なセンチメントヒューリスティック:ルールベースエンジンは、VADERやHugging Faceの distilbert-base-uncased-finetuned-sst-2-english のようなトランスフォーマーモデルをサポートするようアップグレード可能。

BMW AutoTrend Dashboardコードベースの技術ドキュメントとして作成。Githubリポジトリ: https://github.com/scha54/BMW-AutoTrend-Dashboard