ローカルLLM(NVIDIA DGX Spark)× PostgreSQL + pgvector で、約8.2万件の落札を1件も取り違えない名寄せエンジンと、12万ベクトルの意味検索を回している
newsatsu.jpの中身を、実データ付きで全部書きます。
TL;DR(3行)
- 全国の公共工事の入札・落札データを構造化して検索できる無料ポータル
newsatsu.jpの裏側。 - LLM推論は全部ローカル(DGX Spark 2台=GB10 Grace Blackwell)。クラウドAPI課金は1リクエストもゼロ。
- そして一番の見どころは、AIエージェントがコードを書き → 別のAIエージェント48体が“敵対的に”レビューして → 数字が合わないものは正直に「❌反証」として残す、という開発フローそのものです。
「検索サイトの顔をして、中身は全国の入札の勝敗・価格・競争関係を構造化して持つデータ基盤」――この記事はその技術スタックと、データを触って初めて分かった反直感な事実を、隠さず(捏造もせず)書きます。
🧑💻 作っているのは、大企業の開発チームではありません。 オーナー1人が、AIエージェント群を“部下”のように使って、Web・DB・クローラ・AI・検索・予測・インフラ・監視・SEOまで全部ひとつにつないでいます。「1人 × AIエージェント」で、どこまでの規模のデータプロダクトが回るのか――その実例としても読んでもらえると嬉しいです。
想定読者:個人開発でローカルLLMを本気運用したい人 / データエンジニアリング・名寄せ・pgvectorに興味がある人 / 「AIにコードを書かせて品質をどう担保するか」を考えている人。
0. これは何のサイト?
newsatsu.jp は、日本の公共工事の入札公告・落札結果を横断検索できる、登録不要・完全無料のポータルです。
- 各自治体・発注機関の電子入札ポータル(NEC系 ejPCJ、JACIC系 i-PPI、県・市の独自サイト等)を自動でクロール
- PDF/HTMLから「案件名・予定価格・落札者・落札額・工種・場所」を構造化抽出
- 会社単位・発注機関単位・工種単位・地図上で横断的に見られる
スケール感(2026-06時点のスナップショット):
| 指標 | 値 |
|---|---|
| 集約済みの入札案件 | 102,380 件 |
落札結果(会社へ正規化済 v_company_awards=81,979) |
82,627 件 |
| 掲載建設会社 | 12,394 社 |
入札参加の試行ログ(敗者込み)bid_attempts
|
341,669 行 / 4,362社 / 52,606入札 |
| 意味検索用ベクトル(bge-m3, 1024次元) | 123,873 本 |
| 対象市場規模(落札額の合計, 2023–2026) | 約 ¥3.11 兆 |
| 情報網羅率 | 愛知 97.1% / 岐阜 82.7% / 静岡 68.1% |
| 稼働県 | 東海3県(愛知・岐阜・静岡)→ 全国展開中 |
数字を見ると「ただのスクレイピング集計サイトでしょ?」と思うかもしれません。そこが本題で、面白いのはここからです。
1. 技術スタック全体像
まず地図を出します。全部 Docker Compose で1台(+推論用にもう1台)に同居しています。
| レイヤ | 採用技術 |
|---|---|
| フロント | Next.js 15(App Router / TypeScript)、Clerk(認証)、Tailwind、Leaflet(地図ヒートマップ)、Recharts、SWR |
| 配信 | Cloudflare(CDN + Tunnel + Cache Rules)、ISR/SSR 併用、動的サイトマップ |
| API | FastAPI(Python)、ルーター分割、内部キーで管理系を保護 |
| DB |
PostgreSQL 16 + pgvector(pgvector/pgvector:pg16)、HNSW索引、マテビュー多数 |
| 周辺 | Redis(キャッシュ/キュー)、MinIO(S3互換, 生バイナリ)、Elasticsearch、Grafana/Loki、Open WebUI |
| クローラ |
Python + Playwright(ヘッドレス)+ APScheduler、ポータル別の PortalConfig レジストリ |
| 推論 | Ollama(gpt-oss:20b/120b、gemma 12B、GLM-4.7-Flash、bge-m3、qwen3-vl、RapidOCR)をローカルGPUで |
| インフラ | Docker Compose、2台を Tailscale メッシュで接続 |
ちなみにコード規模は クローラ Python 233ファイル / API(FastAPI)20ルーター / Web(Next.js App Router)136ファイル。スパイダー・PDF解析・参加資格判定・ML・AI生成・名寄せ・監査が、それぞれ独立モジュールに分かれています。
「Next.js + FastAPI + Postgres」までは普通です。普通じゃないのはこの先。
2. 工夫①:推論API課金をゼロにするローカルLLM構成
このシステム、LLMをめちゃくちゃ使います。案件要約・会社概要生成・工種分類・参加資格判定・意味検索の埋め込み・画像/CAPTCHAのデコード……。普通にOpenAIやAnthropicのAPIに投げたら、月の請求が想像したくない金額になります。
ここで効いているのが NVIDIA DGX Spark(GB10 Grace Blackwell・128GBクラスのユニファイドメモリ)2台です。本番ノードは 20コア / RAM 実測 約121GB。
-
spark2= 本番(DB・API・Web・クローラ) -
spark1= 推論専用(ほぼ常時アイドル → 計算資源として叩く) - 2台を Tailscale でメッシュ接続し、推論は近い方/空いている方へ
128GBユニファイドメモリのおかげで、1台のGB10に複数モデルを常駐できます(実測でgemma 約10GB + gpt-oss:20b 約13.6GB + qwen3 約45GB を同時ロード)。これで「モデルの載せ替えスラッシング」を避けつつ、用途別にモデルを使い分けています。
| 用途 | モデル | 備考 |
|---|---|---|
| 工種分類(最も重い) | gemma 12B | gpt-ossの約2倍速 |
| 参加資格判定(最重要) | gpt-oss:20b | スキーマ遵守率が高く据え置き |
| 案件要約 / 会社概要 | gemma 12B | 旧gpt-oss:120bから移行(後述) |
| AI解説記事の生成 | GLM-4.7-Flash | 120bより約10倍速・1日6本を自動生成 |
| 意味検索の埋め込み | bge-m3 | 1024次元・多言語 |
| 画像 / CAPTCHA | qwen3-vl | 同期処理 |
クラウドAPI課金=0。しかもデータは1バイトも外に出ない。スタートアップ的には、これだけで競争優位になります。
「指揮官」=GPUオーケストレータ
ただGPUが2台あるだけでは遊休が出ます。そこでジョブをGPUに割り振る“指揮官” を自作しました。
-
gpu_availability.py… Ollamaの/api/psを2分おきに叩き、「常駐warm」と「いま稼働中」をexpires_atで弁別してスナップショット化 -
gpu_dispatcher.py… スナップショット+バックログ(未処理ジョブ)を見て、アイドルなspark1へ最優先で投入。次の予約ジョブの25分前は割り込ませない安全弁つき
そして実装でハマった地味だけど重要なバグ:Ollamaが返すexpires_atはナノ秒9桁で、Python 3.10のdatetimeがパースできず、稼働中GPUを「アイドル」と誤判定 → そこへ二重投入して競合、という事故が起きました。6桁に丸めて根治。こういう「外部APIの日時フォーマット」で死ぬの、あるあるですよね。
ジョブキューは“中央ディスパッチャ無し”で組む
非同期AIジョブ(ai_jobsテーブル)は、中央のディスパッチャを置かず、各ワーカが自分でキューから引くプル型にしています。PostgreSQLだけで安全な並行処理を実現する定番パターン:
-- 各ワーカが空いた瞬間に1件“だけ”ロックして引く(取り合いを起こさない)
SELECT id, payload
FROM ai_jobs
WHERE status = 'PENDING'
ORDER BY priority DESC, created_at
FOR UPDATE SKIP LOCKED
LIMIT 1;
さらに:
-
single-flight dedup:同じ入力のハッシュに
pg_advisory_xact_lockを取り、同一入力の二重推論を防止 -
結果キャッシュ
ai_results:input_hash + 用途 + model_name + model_digest + プロンプト版をキーにして、同じ問いには推論しない -
リース&reaper:ワーカが落ちてもハートビート切れで自動回収、
status='RUNNING'チェックでゾンビの二重適用をフェンシング
「ローカルLLM × PostgreSQLジョブキュー」は、APIコスト気にせずいくらでも回せるので、個人開発〜小規模チームに本当におすすめの構成です。
3. 工夫②:同名異社を混ぜない名寄せ不変量(8.2万件を1件も取り違えない)
ここが、このプロジェクトで一番“血が流れた”ところです。
公共工事のデータには、同じ名前の別会社(同名異社)が大量にいます。「山田建設」が愛知にも静岡にも岡山にもある。落札データには社名(商号)しか書いていないことも多い。雑にマッチすると、A社の落札がB社のページに合算され、全部の統計が壊れます。
解いた方法:カスケード名寄せ
「どのソースを信じるか」を優先順位付けしたカスケードを組みました。
① 名簿(入札参加資格者名簿) … 県が持つ最も信頼できる原本。県でアンカーされる
② 建設業許可番号(8桁) … 先頭2桁が県。MLIT(国交省)の許可DB
③ 法人番号 … 国税庁。マスタに本店所在県が埋まっている
④ gBizINFO / 公式サイト … 上場企業・大手向け
⑤ ローカルAI … 構造データが無いときの最終手段
普通に名寄せするだけでも大変ですが、ここで効いた仕掛けが2つあります。
(1) 建設業許可の「全営業所」検索(choice=2)
許可DBは本店だけ引くと同名異社を取り違えます。本店だけでなく全営業所を引くことで、「どの県のどの営業所が、この案件の現場に近いか」で曖昧性を解消。地理(現場↔本店/営業所)・工種・発注機関への過去落札の親和性で判定し、検証データで約92〜97%の一致を達成しました。
(2) 「キメラ検出」と likely_split = 0 という不変量
会社の整合性を監視するビュー v_company_integrity を作り、「県のズレ」をシグナルに同名異社の“キメラ”を検出します。
例:ある「青山型」のキメラ。1つのDB行が、静岡・奈良・名古屋の3つの別法人を混ぜて持っていた。→ 正しい法人番号で3行に分解。
ロジックはシンプルで強力:
-- 県の許可を持つのに、法人番号マスタの本店県が食い違う = 分裂キメラの疑い
SELECT *
FROM v_company_integrity
WHERE verdict = 'likely_split';
-- これが常に 0 になるようにシステムを保つ
そして全工程で守る不変量がこれ:
SELECT (SELECT count(*) FROM v_company_awards) AS vca, -- = 81,979
(SELECT count(*) FROM v_company_integrity
WHERE verdict = 'likely_split') AS split; -- = 0
VCA = 81,979(落札の正帰属件数)と likely_split = 0(キメラ0件)を、あらゆるDB変更の前後で必ず確認する。確定した同名異社を付け替えても、大手の法人番号を直しても、累計数百件の修正を通してこの2つの数字を一度も壊さなかった。データ基盤の品質を、人間の「気をつける」ではなく機械的な不変量で守るのが肝です。
名寄せって地味ですが、ここが100%正しくないと上に乗る全機能(落札率・会社平均・市場シェア・予測)が全部嘘になる。データ事業の土台は、結局ここの泥臭さで決まります。
4. 工夫③:pgvector / HNSW を使った意味検索(眠れる12万ベクトルの起動)
埋め込みモデル bge-m3 で全案件タイトル等を1024次元ベクトル化し、123,873本を pgvector の HNSW索引に積んでいます。
-- ある案件に「意味が似た案件」を、コサイン距離で近傍検索
SELECT p.id, p.title
FROM ai_embeddings e
JOIN projects p ON p.id = e.entity_id
WHERE e.kind = 'project_title'
ORDER BY e.embedding <=> $query_vec -- HNSW索引が効く
LIMIT 10;
面白い(そして自戒的な)のは、**この最高ROIの資産が、長らく“眠っていた”**こと。索引も埋め込みも常時更新されているのに、公開検索はILIKEの部分一致のままで、ベクトルを使う消費者がほぼゼロだった時期がありました。
ローカルGPUならクエリ側を埋め込むだけで追加計算はほぼ0。「重い側(インデックス構築)は全部できているのに、軽い側(クエリ埋め込み)を配線していないだけ」――この“もったいない”を棚卸しで見つけて潰す、というのもデータ基盤運用のリアルです。
5. 工夫④:LLMの捏造を“未取得”として扱う設計(ハルシネーション監査)
LLMで会社概要を自動生成すると、それっぽい嘘を吐きます。ここを定量監査しました。
会社概要 n=362 を盲検A/B(1ペアを3人の審判エージェントが判定=183 agent-judges)で評価した結果:
| モデル | 捏造率 |
|---|---|
| 旧 gpt-oss:120b | 96.4% 🚨 |
| 新 gemma 12B | 0% ✅(審判全会一致100%) |
gpt-oss:120bの主な捏造パターンが教訓的でした:
- 法人マスタの**「種別コード」(301/302=法人の構造種別)を“法人格”だと誤読**して創作
- 完工高など数値を勝手に発明(45.5億円 → 27.61万円のような取り違え)
→ gemmaへ全面移行し、デュアルGPUで OLLAMA_NUM_PARALLEL=3 にして再生成を並列化(110件/時 → 540件/時、約5倍)。
そしてこのプロジェクトの一貫した思想が 「捏造より未取得を正直に(捏造より“—”を出せ)」:
- 生成に失敗した概要は、それっぽい定型文を埋めず
—(ハイフン) を表示 - 公告日が取れていない過去案件(実は8〜9割)は、取り込み日で埋めず「—」
- 予定価格が取れない案件は「未取得」と正直に。間違った値より、空白の方がいい
SEOの成長は鈍るかもしれません。でもユーザーの信頼は捏造で買えない。データ事業としては、これがいちばん効く差別化だと思っています。
6. 工夫⑤:AI生成コードを敵対レビューする監査フロー
ここが、エンジニアに一番刺さると思う部分。このシステムの大半は、AIエージェント(Claude)が書いて、AIエージェントが運用しています。 ただし「AIに丸投げしてヨシ!」ではありません。AIが自分の成果物を敵対的に検証する仕組みを組んでいます。
48エージェントの並列・敵対監査
全コード・全データの総監査を、こうやって回しました:
- 本体が30分かけて実機でデータ地図を確認(
ssh→psqlでread-only) - Workflowで48エージェントを並列に走らせ、軸A〜H(データ品質/スキーマ性能/ML/ローカルAI/クローラ/API・Web・SEO/セキュリティ/新データ商品)を横断スキャン
- 各発見を“別の”エージェントが独立に再クエリして敵対検証
結果:76発見 / 2,512,839トークン / 1,026ツール実行 / 22分。検証バッジを付与しました:
- ✅ 確認(独立再現できた):26件
- ❌ 反証(主張を棄却。正直に残す):2件
- ⚠️ 一部訂正(方向は正しいが数字を実機で締め直し):多数
「自分の間違いを、隠さず残す」
監査レポートには**“正直さの章”**があり、敵対検証で棄却・訂正された自分の主張をわざわざ載せています。例:
- ❌ 「この106MBの主キー索引は死んでいる(1 scan)」→ 逆。表で最も使われる索引(674,320 scan)だった。本当に未使用なのは別の索引。
- ⚠️ 「再入札の99.5%が値下げ」→ 94.3%が値下げ・5.5%は値上げに訂正。「round2は必ず下がる」前提のモデルは18回に1回外す。
「AIが出した結論を、別のAIが数字で殴り返し、負けたら正直に取り下げる」。これをCIのように回すと、AI開発の最大の弱点である**“それっぽい嘘”が本番に届く前に死にます**。生成AI時代の品質保証は、たぶんこの形になっていきます。
自律運用と自己修復
監査だけでなく日常運用も自律化しています:
- 県別の自律進出オーケストレータ:状態機械(survey → spider草案 → テスト合格 → backfill → 品質チェック → 完了)を回し、5指標90%のゲートを満たさないと次に進めない
- 自己修復:品質スコアの自動再測定(あるバグで38.5に凍結 → 修正で57.0に)、失敗した生成の自動リトライ、毎日のキメラ監査
- 品質の停滞/退行を自動検知する県別オーディタ(停滞・退行・目標未達を3指標で監視)
7. 番外編:実データが「業界の常識」を壊す(経審神話・偽の落札率・非単調な勝率)
データが本物だと、みんなが信じている通説が実は間違っていることが見えてきます。匿名・集計ベースで2つだけ。
「経審が高い=落札しやすい」は、このデータでは概ね誤り
経営事項審査(経審)の総合P値で会社を十分位に分けて勝率を見ると:
| 分位 | 勝率 |
|---|---|
| 第1〜9分位(=9割の会社) | 15〜18%でほぼフラット |
| 上位10%だけ | 23% |
相関係数は +0.29(弱い)。つまり**「経審が高いほど勝つ」は、上位1割でようやく頭が出るだけ**。中堅企業の勝率は大手とほぼ変わらない。「経審スコアだけで序列化して煽る」のはミスリードで、逆に中小にも勝ち目があることのデータ的裏付けになります(※この知見は愛知のデータが中心である旨を明記した上での話です)。
「落札率」の7割が偽物の 1.0 だった
最大市場・愛知の落札率(award_rate)が、62,937件中43,814件(70%)でぴったり1.0。一見「落札率100%だらけ」ですが、これは予定価格が取れなかった時のプレースホルダでした。
そのうち 33,662件は 落札額 ÷ 予定価格 で真の落札率を即復元可能(復元後の中央値 0.94)。元のコードが award_rate IS NULL の行しか埋めない設計だったため、この1.0を永遠にスキップしていたのです。
-- 偽の 1.0 を、復元可能な行だけ真の落札率へ(既存ガードを再利用・可逆)
UPDATE awards a
SET award_rate = round(a.bid_amount / p.scheduled_amount, 4)
FROM projects p
WHERE a.project_id = p.id
AND a.is_winner
AND a.award_rate = 1.0
AND a.bid_amount < p.scheduled_amount -- 真の1.0(bid=scheduled)は触らない
AND a.bid_amount / p.scheduled_amount >= 0.3;
これを直すと、愛知の“使える落札率”が153件 → 約42,000件に化けました。上に乗る分析(落札率分布・会社平均・市場レポート)が、ここで初めて統計的に意味を持ちます。「集めて終わり」ではなく、集めた後の品質との戦いが本番だという好例です。
ML:勝率は「価格」に対して非単調
入札試行ログ(敗者込み)を予定価格で正規化して P(落札 | 価格比) を実測すると、勝率は単調ではないことが分かります。高すぎれば負け、低すぎれば失格、という二重の崖がある。
価格モデル自体も、落札率の分位を LightGBMの分位回帰 で条件付けして、ウォークフォワード・バックテストで現行の静的バンドを pinball loss で +18.6〜27.5% 上回りました(愛知・3 fold)。一方で「メタデータから予定価格(円)をピタリ当てる」のは、同一セル内でも予定価格が約19倍ばらつくため原理的に無理と分かり、**“当たらないものは作らない”**と正直に結論づけました。ここでも思想は一貫しています。
8. ハマったところ・学び(実話)
バズ記事だからこそ、格好いいだけじゃなく転んだ話も。
-
外部APIの日時フォーマット:Ollamaの
expires_atがナノ秒9桁でPythonがパース不能 → GPUを誤って「アイドル」判定 → 二重投入で競合。6桁丸めで根治。 -
「
award_rate IS NULLしか埋めない」設計の罠:7割の偽1.0を永久スキップ。“NULLじゃないが間違っている”データは、NULLより厄介。 - 常駐プロセスのコードが更新されない:クローラはbind-mountで即反映なのに、スケジューラ本体はコンテナを作り直すまで古いコードのまま動く。「直したのに直らない」の典型。
- 締切切れの公告が再openする:閉じる処理の後でクローラが締切超過の公告を再度openし、「もう入れない案件」を入札可能と表示。冪等ガードで抑制。
-
集計は entity 解決済みビューでやる:生の
company_idでGROUP BYすると同名異社が混ざる。必ずv_company_awards経由。
どれも「あー、それ踏んだことある」と頷ける泥臭さだと思います。データ基盤は、結局この積み重ねです。
9. まとめ ―― なぜこれが“偽装できない”優位なのか
newsatsu.jp の技術的な凄みを一言で言うと、
「ローカルAIで推論コストを0にし、AIが自分を敵対レビューしながら、機械的な不変量で品質を守る」
という、生成AI時代のデータ基盤の一つの完成形を、地味で巨大な公共入札という領域で実装していることです。
- ローカルLLM(DGX Spark / GB10) … 推論はいくら回しても0円・データは外に出ない
-
同名異社の名寄せ …
VCA=81,979 / likely_split=0を不変量に、8.2万落札を1件も取り違えない - pgvector + HNSW … 12万ベクトルの意味検索
- 捏造より未取得を正直に … ハルシネーション率 96% → 0% を盲検A/Bで実証
- AIの敵対的セルフレビュー … 48エージェント・250万トークンで、自分の結論を自分で棄却する
クロールは真似できても、「8万件を1件も取り違えない名寄せ」と「自分の嘘を潰す監査文化」は、一朝一夕には積み上がりません。データ事業の堀(moat)は、派手な機能ではなく、こういう地味な正確性の総和に宿るのだと思います。
そして念のため言うと、これを回しているのは オーナー1人 + AIエージェント群。いま東海3県で動いているこの基盤を全国47都道府県へ広げれば、ただの便利サイトではなく、“建設業界版の Bloomberg / SPEEDA / 帝国データバンク” に化ける余地がある――そう本気で思っています。newsatsu.jp、ぜひ一度触ってみてください。
おまけ:使った技術タグ
Next.js FastAPI PostgreSQL pgvector Ollama LLM NVIDIA DGX Spark GB10 bge-m3 LightGBM データエンジニアリング 名寄せ 生成AI AIエージェント
質問・ツッコミ・「うちはこう名寄せしてる」みたいな話、コメントで大歓迎です。
役に立ったら LGTM で応援してもらえると、続編(pgvector索引チューニング編 / ローカルLLMジョブキュー実装編)を書く励みになります🙏