1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【無料】ローカルLLMでベクトルDBを使ってみる

1
Posted at

【無料】ローカルLLMでベクトルDBを使ってみる

動作環境・使用するツールや言語

  • Windows 11 Home 25H2
  • RAM 16.0GB
  • NVIDIA GeForce RTX 3050 Ti Laptop GPU (4 GB)
  • Oracle 26ai free
  • Python

この記事の対象者

  • Python初心者
  • AI初心者
  • 普段Oracleを使っている人

はじめに

AIの学習の手始めとして、無料かつローカルで気軽に試せる環境で動作確認していきます。
プロセスを確認しながら仕組みを学習するのが目的なので、LlamaIndexのようなフレームワークは今回使いません。

ベクトルDBとは

文章や画像などの非構造化データをベクトル座標で表してテーブルのレコードに投入したものです。
座標的に近いものを意味が近いものとして取得できるようになります。
RAGとセットで説明されることが多いですが、RAGは外部からデータを取ってきてLLMに渡す仕組みなので、ベクトルDBが必須というわけではありません。
LLMもベクトル表現を使って学習・推論しているので仕組みとしては似ていますが、直接データを保持しているわけではなく、学習済みパラメータとして保持しています。
LLM内にはコンテキストウィンドウというメモリ領域はありますが、大量のデータを保持できるわけではありません。
従来のシステム構成に例えるなら、LLMはAPサーバ、ベクトルDBはDBサーバ、RAGはAPサーバからSQLを発行してDBサーバに問い合わせるアーキテクチャのようなものです。

Oracle 26ai freeの準備

公式サイトからwindows版をダウンロードしてインストールします。
初期状態だとlistener.oraやtnsnames.oraのIPアドレスがプライベートIPアドレスになるので、127.0.0.1に修正して再起動します。
sys as sysdbaで接続して

CREATE TABLE test_vector (
    id NUMBER,
    embedding VECTOR(3, FLOAT32)
);

が通ればベクトルDBが使えます。
次に、PDBの名称(おそらくFREEPDB1)を確認してopenしていなければopenします。

SHOW PDBS
ALTER PLUGGABLE DATABASE FREEPDB1 OPEN;
ALTER SESSION SET CONTAINER = FREEPDB1;
ALTER PLUGGABLE DATABASE FREEPDB1 SAVE STATE;

PDBはTNS接続しか受け付けないのでtnsnames.oraに追記します。

RAG =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = 127.0.0.1)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = freepdb1)
    )
  )

PDBにSYSTEMで接続してパスワードの期限切れを防いでからユーザを作成します。
とりあえずユーザ名もパスワードもragにしておきます。

ALTER PROFILE DEFAULT LIMIT PASSWORD_LIFE_TIME UNLIMITED;
ALTER PROFILE DEFAULT LIMIT FAILED_LOGIN_ATTEMPTS UNLIMITED;
ALTER PROFILE DEFAULT LIMIT PASSWORD_LOCK_TIME UNLIMITED;
ALTER PROFILE DEFAULT LIMIT PASSWORD_GRACE_TIME UNLIMITED;
CREATE USER rag IDENTIFIED BY rag;
GRANT CONNECT, RESOURCE TO rag;
ALTER USER rag
DEFAULT TABLESPACE USERS
QUOTA UNLIMITED ON USERS;

ragユーザで接続してベクトルDBのテーブルを作成してみます。
なおVECTOR型は内部的にSecureFiles LOBを使用しており、これはASSM(自動セグメント管理)でなければ作成できません。

CREATE TABLE documents (
    id NUMBER GENERATED ALWAYS AS IDENTITY,
    content CLOB,
    embedding VECTOR(384, FLOAT32)
);

Pythonで挙動の確認

Pythonがなければインストールします。
IDEやライブラリも必要に応じてインストールします。

Oracleのエラーメッセージ「ORA-01555 snapshot too old」をベクトルに変換し、listにし、それをjson文字列に変換すればTO_VECTOR()でINSERTできます。
ベクトル変換は今回embeddingモデル「multilingual-e5-small」を使用します。
日本語対応で無料、処理能力は低いが高精度とのことです。
実行時にWARNINGが出ますが、Hugging Faceという会社から匿名でダウンロードしているので、レート制限やダウンロード速度低下の可能性が出ることに対する警告なのでテストでは無視して構いません。
消す場合は無料アカウントを作成してログインするかトークンを指定する必要があります。

以降、ライブラリのインポートは省略します。

import oracledb
import json
from sentence_transformers import SentenceTransformer

# Embeddingモデル
model = SentenceTransformer('intfloat/multilingual-e5-small')

# Oracle接続
conn = oracledb.connect(
    user="rag",
    password="rag",
    dsn="127.0.0.1:1521/FREEPDB1"
)

cursor = conn.cursor()

# テキスト
text = "ORA-01555 snapshot too old"

# Embedding生成
embedding = model.encode(text).tolist()

# JSON文字列化
embedding_json = json.dumps(embedding)

# INSERT
sql = """
INSERT INTO documents(content, embedding)
VALUES (:1, TO_VECTOR(:2))
"""

cursor.execute(sql, [text, embedding_json])

conn.commit()

print("insert completed")

cursor.close()
conn.close()

上記でINSERTしたものをSELECTします。
CONTENTはCLOB型なので、READする必要があります。
ここでは「UNDO不足」という文章を打つと、それをベクトル変換してdocuments内のレコードと比較しコサイン類似度(ベクトル的な角度、方向が近い)が近いものを出力しています。

import oracledb
import json
from sentence_transformers import SentenceTransformer

# Embeddingモデル
model = SentenceTransformer('intfloat/multilingual-e5-small')

# Oracle接続
conn = oracledb.connect(
    user="rag",
    password="rag",
    dsn="127.0.0.1:1521/FREEPDB1"
)

cursor = conn.cursor()
query = "UNDO不足"
query_embedding = model.encode(query).tolist()
query_embedding_json = json.dumps(query_embedding)

sql = """
SELECT content,
       VECTOR_DISTANCE(
           embedding,
           TO_VECTOR(:1),
           COSINE
       ) distance
FROM documents
ORDER BY distance
FETCH FIRST 3 ROWS ONLY
"""

cursor.execute(sql, [query_embedding_json])

for row in cursor:
    content = row[0].read()
    distance = row[1]

    print(content)
    print(distance)

ファイルから読み取る場合はテキストファイルを下記のように用意します。
空行で区切り、一つのチャンク(塊)とします。
チャンク単位でベクトル変換するので、意味的にまとまりがあるようにした方がよいです。
ここでは1エラー=1チャンク=1レコードとしています。

ora_errors.txt
ORA-01555 snapshot too old
UNDO不足や長時間トランザクションで発生

ORA-01652 unable to extend temp segment
TEMP表領域不足

ORA-00054 resource busy
ロック競合

PythonのINSERTは下記の通りです。単にループ処理にしただけです。

import oracledb
import json
from sentence_transformers import SentenceTransformer

# Oracle接続
conn = oracledb.connect(
    user="rag",
    password="rag",
    dsn="127.0.0.1:1521/FREEPDB1"
)

cursor = conn.cursor()

# Embeddingモデル
model = SentenceTransformer('intfloat/multilingual-e5-small')

# テキストファイル読込
with open("ora_errors.txt", "r", encoding="utf-8") as f:
    text = f.read()

# 空行区切りで分割
chunks = text.split("\n\n")

for chunk in chunks:

    chunk = chunk.strip()

    if not chunk:
        continue

    # Embedding生成
    embedding = model.encode(chunk).tolist()

    embedding_json = json.dumps(embedding)

    # INSERT
    sql = """
    INSERT INTO documents(content, embedding)
    VALUES (:1, TO_VECTOR(:2))
    """

    cursor.execute(sql, [chunk, embedding_json])

conn.commit()

print("completed")

cursor.close()
conn.close()

さらにORA-エラーをある程度公式サイト(英語)から集めてora_errors.txtに貼り付けます。(手動でコピペ)
INSERT後「UNDO不足の対処法」でSELECTすると以下のような出力を得ます。

ORA-01555
snapshot too old: rollback segment number string with name "string" too small
Cause
rollback records needed by a reader for consistent read are overwritten by other writers
Action
If in Automatic Undo Management mode, increase undo_retention setting. Otherwise, use larger rollback segments
0.17660124033109692
ORA-30306
Internal error
Action
Not a user error.
0.18664410454076386
ORA-01652
unable to grow segment_type object_name in tablespace tablespace_name by storage_allocatedstorage_units during operation with SQL ID : sql_id, temp space used by session : string (MB)
segment_type: The type of the segment that ran into failure. The type may be one of temporary, table, table partition, table subpartition, index, index partition, index subpartition, LOB, LOB partition, or LOB subpartition.
object_name: The object name if the failure is in an object that is already created.
tablespace_name: The name of the tablespace that was supposed to be extended during the operation.
storage_allocated: The number of bytes in KB, MB, or GB in which the allocation was attempted.
storage_units: The unit of bytes in KB, MB, or GB in which space was allocated.
sql_id: SQL ID of the session for which error was received.
Temp Space Allocated: Temporary space currently used by the session.
Cause
In order to execute the operation, additional space is needed in the tablespace but the system is unable to increase the tablespace size. This can happen if the tablespace size is small. In a temporary tablespace, this can also happen because of concurrent use of the tablespace.
Action
If the failure is in a temporary tablespace, then retry the operation in case any concurrent operations have released space. For failure in permanent and temporary tablespaces, the size of the tablespace can be increased using one of the following methods:
Resize the tablespace using either the ALTER DATABASE RESIZE or ALTER TABLESPACE ADD statement.
Enable AUTOEXTEND for the tablespace.
If AUTOEXTEND is already enabled, then:
If MAXSIZE is set to UNLIMITED, increase the storage media where the tablespace is located.
Increase MAXSIZE.
If it is a BIGFILE tablespace, then use the ALTER TABLESPACE
RESIZE statement to increase the tablespace size.
0.1995301193006519

見ての通り、中身はエラーコード、概要、エラーの原因、対処法について記載されています。
対処法について聞いても、1エラー=1チャンク=1レコードなので全文を出力することしかできません。
エラーコード+概要、原因、対処法でそれぞれチャンクを分けてみましょう。以下のように空行で区切るだけです。

ora_errors.txt
ORA-01555
snapshot too old: rollback segment number string with name "string" too small

Cause
rollback records needed by a reader for consistent read are overwritten by other writers

Action
If in Automatic Undo Management mode, increase undo_retention setting. Otherwise, use larger rollback segments
・・・(省略)

ORA-で始まるCONTENTは要約、
CAUSEで始まるCONTENTは原因、
ACTIONで始まるCONTENTは対処としてカテゴライズできます。
メタデータとしてCATEGORY列を追加します。

DROP TABLE documents;

CREATE TABLE documents (
    id NUMBER GENERATED ALWAYS AS IDENTITY,
    error_code VARCHAR2(20),
    category VARCHAR2(20),
    content CLOB,
    embedding VECTOR(384, FLOAT32)
);

今回は泥臭く正規表現で処理します。LLMに渡してJSON文字列を返させて…というのでもいいですが、それは後でやります。
re.splitでファイル全体の文字列textを分割して配列blocksに格納します。
エラーごとに分割するため「ORA-xxxxx」で分割しています。
re.Sはre.DOTALLの別名で、正規表現のドットが改行文字を含むすべての文字にマッチするようになります。

import oracledb
import json
import re

from sentence_transformers import SentenceTransformer

# Oracle接続
conn = oracledb.connect(
    user="rag",
    password="rag",
    dsn="127.0.0.1:1521/FREEPDB1"
)

cursor = conn.cursor()

# Embeddingモデル
model = SentenceTransformer('intfloat/multilingual-e5-small')


# テキストファイル読込
with open("ora_errors.txt", "r", encoding="utf-8") as f:
    text = f.read()

blocks = re.split(r'(?=ORA-\d{5})', text)

for block in blocks:
    if not block.strip():
        continue

    error_code = re.search(r'(ORA-\d{5})', block)

    if error_code:
        error_code = error_code.group(1)

    summary_match = re.search(
        r'ORA-\d{5}\s+(.*?)\s+Cause',
        block,
        re.S
    )
    
    cause_match = re.search(
        r'Cause\s+(.*?)\s+Action',
        block,
        re.S
    )

    action_match = re.search(
        r'Action\s+(.*?)(?:Additional Information|$)',
        block,
        re.S
    )
    
    # ここからsummary処理
    summary = summary_match.group(1).strip() if summary_match else ""
    
    if summary:
        # Embedding生成
        embedding = model.encode(summary).tolist()

        embedding_json = json.dumps(embedding)

        # INSERT
        sql = """
        INSERT INTO documents(error_code, category, content, embedding)
        VALUES (:1, :2, :3 ,TO_VECTOR(:4))
        """

        cursor.execute(sql, [error_code,"SUMMARY", summary, embedding_json])


    # ここからcause処理
    cause = cause_match.group(1).strip() if cause_match else ""
    
    if cause:
        # Embedding生成
        embedding = model.encode(cause).tolist()

        embedding_json = json.dumps(embedding)

        # INSERT
        sql = """
        INSERT INTO documents(error_code, category, content, embedding)
        VALUES (:1, :2, :3 ,TO_VECTOR(:4))
        """

        cursor.execute(sql, [error_code,"CAUSE", cause, embedding_json])

    # ここからaction処理
    action = action_match.group(1).strip() if action_match else ""

    if action:
        # Embedding生成
        embedding = model.encode(action).tolist()

        embedding_json = json.dumps(embedding)

        # INSERT
        sql = """
        INSERT INTO documents(error_code, category, content, embedding)
        VALUES (:1, :2, :3 ,TO_VECTOR(:4))
        """

        cursor.execute(sql, [error_code,"ACTION", action, embedding_json])

conn.commit()

print("completed")

cursor.close()
conn.close()

検索の処理は下記の通りです。

import oracledb
import json
from sentence_transformers import SentenceTransformer

# Embeddingモデル
model = SentenceTransformer('intfloat/multilingual-e5-small')

# Oracle接続
conn = oracledb.connect(
    user="rag",
    password="rag",
    dsn="127.0.0.1:1521/FREEPDB1"
)

cursor = conn.cursor()

query = "UNDO不足の対処法"

query_embedding = model.encode(query).tolist()

query_embedding_json = json.dumps(query_embedding)

sql = """
SELECT ERROR_CODE,CATEGORY,content,
       VECTOR_DISTANCE(
           embedding,
           TO_VECTOR(:1),
           COSINE
       ) distance
FROM documents
ORDER BY distance
FETCH FIRST 3 ROWS ONLY
"""

cursor.execute(sql, [query_embedding_json])

for row in cursor:
    error_code = row[0]
    category = row[1]
    content = row[2].read()
    distance = row[3]

    print(error_code)
    print(category)
    print(content)
    print(distance)

出力は下記のようになります。
クエリが「UNDO不足の対処法」で、上位にACTIONのカテゴリーが来ていることが確認できます。

ORA-01555
ACTION
If in Automatic Undo Management mode, increase undo_retention setting. Otherwise, use larger rollback segments
0.1481109923305981
ORA-01652
ACTION
If the failure is in a temporary tablespace, then retry the operation in case any concurrent operations have released space. For failure in permanent and temporary tablespaces, the size of the tablespace can be increased using one of the following methods:
Resize the tablespace using either the ALTER DATABASE RESIZE or ALTER TABLESPACE ADD statement.
Enable AUTOEXTEND for the tablespace.
If AUTOEXTEND is already enabled, then:
If MAXSIZE is set to UNLIMITED, increase the storage media where the tablespace is located.
Increase MAXSIZE.
If it is a BIGFILE tablespace, then use the ALTER TABLESPACE
RESIZE statement to increase the tablespace size.
0.19967250542901716
ORA-01652
SUMMARY
unable to grow segment_type object_name in tablespace tablespace_name by storage_allocatedstorage_units during operation with SQL ID : sql_id, temp space used by session : string (MB)
segment_type: The type of the segment that ran into failure. The type may be one of temporary, table, table partition, table subpartition, index, index partition, index subpartition, LOB, LOB partition, or LOB subpartition.
object_name: The object name if the failure is in an object that is already created.
tablespace_name: The name of the tablespace that was supposed to be extended during the operation.
storage_allocated: The number of bytes in KB, MB, or GB in which the allocation was attempted.
storage_units: The unit of bytes in KB, MB, or GB in which space was allocated.
sql_id: SQL ID of the session for which error was received.
Temp Space Allocated: Temporary space currently used by the session.
0.20111434810325368

1エラー=1チャンクの場合、原因や対処法について質問しても回答(content)はチャンクの全文が出力されますが、チャンクを分けることによって原因について回答させたり対処法について回答させたりすることができます。
SQL文のwhere句にcategory='Cause'などを入れて絞ることも可能です。
キーワード検索とベクトル検索を合わせることを一般にハイブリッド検索と言います。

ローカルLLMの導入

ベクトルDBをある程度触ったところで、ローカルLLMと連携してみましょう。
まずollama(ローカル環境でLLMを実行できるツール)をインストールします。
LLMについては今回はgemma(Googleの無料LLM)を使っていますが、PCのGPUやメモリにもよるのでどれを使うかは確認してください。
ダウンロードしてセットアップ後、

ollama pull gemma3:4b

でインストールでき、

ollama run gemma3:4b

で起動します。
Pythonで動作確認してみます。

from ollama import chat

response = chat(
    model="gemma3:4b",
    messages=[
        {
            "role": "user",
            "content": "OracleのUNDOとは何ですか?"
        }
    ]
)

print(response["message"]["content"])

下記のような回答になります。

Oracle の UNDO は、データベーストランザクションがコミットされる前のごくわずかな変更を保持し、ロールバックや回復時に使用するための仕組みです。

以下に、Oracle の UNDO について重要なポイントをまとめます。

**1. なぜ UNDO が必要か?**

* **トランザクションのロールバック:** ユーザーが誤った操作を行った場合や、システムエラーが発生した場合など、トランザクションの一部または全部をロールバック(取り消し)してデータを整合な状態に戻す必要があります。UNDO は、ロールバック時に変更を元に戻せるように、それまでの変更履歴を保存します。
* **回復 (Recovery):** データベースサーバ自体に障害(電源 failure など)が発生した場合、UNDO はトランザクションの実行状態を復旧するのに役立ちます。

**2. UNDO の仕組み**

Oracle では、UNDO データ領域と呼ばれる特別な領域に変更履歴が保存されます。このデータ領域は、以下の要素で構成されています。

* **UNDOTBS1, UNDOTBS2,...:**  データベースのインスタンスごとに複数の UNDO Tablespace が存在し、それぞれが変更履歴を保持します。
* **SYSTEM:** データベース全体の変更履歴を保持するテーブルスペースです。
* **UNDOSTATISTICS:** UNDO Tablespace の統計情報を管理するためのテーブルスペースです。
* **UNDO_INTERNAL:**  内部的な UNDO テーブルスペースで、システムによって使用されます。

**3. UNDO の種類**

* **Full Undo:** トランザクションのすべての変更を保持します。最も一般的な UNDO タイプです。
* **Limited Undo:** トランザクションの特定の変更のみを保持します。パフォーマンスを向上させるために使用される場合がありますが、ロールバックが必要な状況では対応できません。
* **Write-Ahead Logging (WAL) による UNDO:**  Oracle では WAL を使用して、データの変更を記録しています。UNDO は、この WAL に保存された情報に基づいて変更履歴を構築します。

**4. UNDO の設定**

UNDO Tablespace のサイズは、データベースの負荷やトランザクションの要件に応じて調整する必要があります。適切なサイズを設定しないと、ロールバックが失敗したり、回復に時間がかかったりする可能性があります。

**5. UNDO の関連機能**

* **Undo History:** UNDO Tablespace に保存された変更履歴を追跡し、ロールバックまたは回復時の使用状況を確認できます。
* **Flashback Technology:** 過去の状態に戻るための技術で、UNDO を利用して実現されます。

**まとめ**

Oracle の UNDO は、データベースの信頼性と可用性を高めるために非常に重要な仕組みです。ロールバックや回復などの機能を実現するために、システムは変更履歴を保持し、必要に応じてその情報を元にデータを復旧します。

**より詳細な情報について:**

* **Oracle Database Documentation:** [https://docs.oracle.com/database/server/e1/undodb/undohist.htm](https://docs.oracle.com/database/server/e1/undodb/undohist.htm)
* **オラクル技術情報センター:** [https://www.oracle.com/jp/database/technologies/undo-tablespace.html](https://www.oracle.com/jp/database/technologies/undo-tablespace.html)

ご不明な点があれば、お気軽にご質問ください。

ベクトルDBの検索処理に導入してみます。

import oracledb
import json
from sentence_transformers import SentenceTransformer
from ollama import chat

# Embeddingモデル
model = SentenceTransformer('intfloat/multilingual-e5-small')

# Oracle接続
conn = oracledb.connect(
    user="rag",
    password="rag",
    dsn="127.0.0.1:1521/FREEPDB1"
)

cursor = conn.cursor()

query = "UNDO不足の対処法"

query_embedding = model.encode(query).tolist()

query_embedding_json = json.dumps(query_embedding)

sql = """
SELECT ERROR_CODE,CATEGORY,content,
       VECTOR_DISTANCE(
           embedding,
           TO_VECTOR(:1),
           COSINE
       ) distance
FROM documents
WHERE CATEGORY = 'ACTION'
ORDER BY distance
FETCH FIRST 3 ROWS ONLY
"""

cursor.execute(sql, [query_embedding_json])

contexts = []

for row in cursor:
    error_code = row[0]
    category = row[1]
    content = row[2].read()
    distance = row[3]
    
    contexts.append(
        f"""
        ERROR_CODE:{error_code}
        CATEGORY:{category}
        CONTENT:
        {content}
        """
    )
       
context = "\n\n".join(contexts)
print("=== CONTEXT ===")
print(context)
print("===============")
question = "UNDO不足で更新処理が失敗した"
prompt = f"""
以下の情報だけを利用して回答してください。

{context}

質問:
{question}    
"""

response = chat(
    model="gemma3:4b",
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ]
)

print(response["message"]["content"])

出力は下記の通りです。
LLMからの回答は最後の日本語の1行です。

=== CONTEXT ===

        ERROR_CODE:ORA-01555
        CATEGORY:ACTION
        CONTENT:
        If in Automatic Undo Management mode, increase undo_retention setting. Otherwise, use larger rollback segments
        


        ERROR_CODE:ORA-01652
        CATEGORY:ACTION
        CONTENT:
        If the failure is in a temporary tablespace, then retry the operation in case any concurrent operations have released space. For failure in permanent and temporary tablespaces, the size of the tablespace can be increased using one of the following methods:
Resize the tablespace using either the ALTER DATABASE RESIZE or ALTER TABLESPACE ADD statement.
Enable AUTOEXTEND for the tablespace.
If AUTOEXTEND is already enabled, then:
If MAXSIZE is set to UNLIMITED, increase the storage media where the tablespace is located.
Increase MAXSIZE.
If it is a BIGFILE tablespace, then use the ALTER TABLESPACE
RESIZE statement to increase the tablespace size.
        


        ERROR_CODE:ORA-04031
        CATEGORY:ACTION
        CONTENT:
        If using one of the initialization parameters SGA_TARGET, MEMORY_SIZE, or MEMORY_TARGET, increase the value of that parameter. If it is not feasible to increase the parameter, then reduce the value of DB_CACHE_SIZE (if set). If you are not using SGA_TARGET, MEMORY_SIZE, or MEMORY_TARGET, increase the size of the pool that is out of memory. For example, increase SHARED_POOL_SIZE for the shared pool or INMEMORY_SIZE for an IMC heap.
        
===============
エラーコードORA-01555が発生した場合は、自動UNDO管理モードであればundo_retentionの設定を増やしてください。そうでない場合は、ロールバックセグメントのサイズを大きくしてください。

調整してみます。
CATEGORYをCAUSEとACTIONにして30行取得して、プロンプトでは原因と対処法を教えるように指定し、DISTANCEは0.3未満(距離的に近いものだけ取得する)としてみます。

import oracledb
import json
from sentence_transformers import SentenceTransformer
from ollama import chat

# Embeddingモデル
model = SentenceTransformer('intfloat/multilingual-e5-small')

# Oracle接続
conn = oracledb.connect(
    user="rag",
    password="rag",
    dsn="127.0.0.1:1521/FREEPDB1"
)

cursor = conn.cursor()

query = "UNDO不足の対処法"

query_embedding = model.encode(query).tolist()

query_embedding_json = json.dumps(query_embedding)

sql = """
SELECT ERROR_CODE,CATEGORY,content,
       VECTOR_DISTANCE(
           embedding,
           TO_VECTOR(:1),
           COSINE
       ) distance
FROM documents
WHERE CATEGORY IN ('CAUSE','ACTION')
ORDER BY distance
FETCH FIRST 30 ROWS ONLY
"""

cursor.execute(sql, [query_embedding_json])

contexts = []

for row in cursor:
    error_code = row[0]
    category = row[1]
    content = row[2].read()
    distance = row[3]
    
    if distance < 0.3:
        contexts.append(
            f"""
            ERROR_CODE:{error_code}
            CATEGORY:{category}
            CONTENT:
            {content}
            """
        )
       
context = "\n\n".join(contexts)
print("=== CONTEXT ===")
print(context)
print("===============")
question = "UNDO不足で更新処理が失敗した。原因と対処法を教えてほしい"
prompt = f"""
以下の情報だけを利用して回答してください。

{context}

質問:
{question}    
"""

response = chat(
    model="gemma3:4b",
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ]
)

print(response["message"]["content"])

下記のような回答になります。さっきの回答よりは精度が上がっているようです。

UNDO不足で更新処理が失敗した原因と対処法は以下の通りです。

**原因:**

*   **ERROR_CODE:ORA-01555**
    rollback records needed by a reader for consistent read are overwritten by other writers

    これは、読取りトランザクション(SELECT文など)と書き込みトランザクション(UPDATE、INSERT、DELETEなど)が同時に実行される際に、UNDOレコード(ロールバックに必要な情報)が別の書き込みトランザクションによって上書きされてしまう場合に発生します。つまり、一定のトランザクションが長時間実行され、UNDOスペースが枯渇した結果です。

**対処法:**

*   **ERROR_CODE:ORA-01555**
    If in Automatic Undo Management mode, increase undo_retention setting. Otherwise, use larger rollback segments

    自動UNDO管理モードであれば、`undo_retention`設定を増やす。そうでない場合は、より大きなロールバックセグメントを使用する。

    **詳細:**
    `undo_retention`は、トランザクションがコミットされるまで保持されるUNDOレコードの最大秒数を指定します。この値を増やすことで、UNDOレコードをより長く保持し、読取りトランザクションがロックを保持している間の書き込みトランザクションからUNDOレコードを保護できます。
    ロールバックセグメントは、UNDOレコードが格納されるデータ領域です。ロールバックセグメントのサイズを増やすことで、UNDOレコードの格納容量を増やすことができます。

    **推奨:** まずは`undo_retention`設定の増額から試してみてください。

ご質問の状況に当てはまるよう、上記を参考に対処法をご確認ください。

なおここでは最初に「UNDO不足の対処法」でクエリをDBに投げて、その結果を「UNDO不足で更新処理が失敗した」という質問の情報ソースにしています。
これはUNDO不足という質問が来ることを想定してますが、実際にはUNDO不足に関する質問が来るかどうかはわかりません。
先に質問をしてからクエリをDBに投げるのが本来の形です。
例えば「UNDO不足の原因は?」と聞かれたらWHERE句にCAUSEを、「UNDO不足の対処法は?」と聞かれたらWHERE句にACTIONを付けるというように動的に変更させます。
単純な実装では特定の文言とか番号をユーザに入力させるようにしてもいいし、LLM自身に判定させるやり方もあります。

インデックスの作成

現在はベクトルDBのすべてを距離計算しています。
ベクトルインデックス(HNSW)を作成してみましょう。
簡単に言うと、近いベクトル同士をつなげて近道できるようにしています。
その前にOracleの初期化パラメータを変更してメモリを確保する必要があります。

CDBに接続して(VECTOR_MEMORY_SIZEはCDBのパラメータ)
パラメータを変更し、再起動します。

ALTER SYSTEM SET VECTOR_MEMORY_SIZE=200M SCOPE=SPFILE;
SHUTDOWN IMMEDIATE;
STARTUP;

PDBに接続してインデックスを作成します。

CREATE VECTOR INDEX documents_hnsw_idx
ON documents(embedding)
ORGANIZATION INMEMORY NEIGHBOR GRAPH
DISTANCE COSINE;

実行計画を確認します。
VECTOR INDEX HNSW SCANになっていればOKです。
TO_VECTORの中身は今回直接作っています。Oracleが用意したモデルを使うことで、DBの中でembeddingすることもできるようですが、free版では使えなさそうです。

EXPLAIN PLAN FOR
SELECT *
FROM documents
ORDER BY VECTOR_DISTANCE(
    embedding,
    TO_VECTOR('[1,2,3]'),
    COSINE
)
FETCH FIRST 3 ROWS ONLY;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

Oracle26aiではHNSW以外にもIVFというインデックスをサポートしています。
簡単に言うとクラスタに分けて、近そうなクラスタだけ検索するものです。
Oracle的にはインデックスというよりパーティションに近いかもしれません。
HNSWは十万~数千万件で精度重視、
IVFは数千万~数億件でスケール重視となっています。

PDFからの取り込み

ここまでテキストファイルを取り込んできましたが、社内の情報を取り込んでデータ化する場合はテキストばかりではないため、試しにPDFを取り込んでみます。
サンプルとして、事前にOracle Database Error Messages(PDF)をダウンロードしておきます。
テーブルも作っておきます。

CREATE TABLE pdf_documents (
    id NUMBER GENERATED ALWAYS AS IDENTITY,
    source_file VARCHAR2(200),
    error_code VARCHAR2(20),
    category VARCHAR2(20),
    content CLOB,
    embedding VECTOR(384, FLOAT32)
);

PDFを読み取ってINSERTします。
今回はORA-エラーのみ取得するのでページを特定しています。
2000ページ以上あるので、INSERTを1行ずつループすると時間がかかります。
executemanyでまとめて実行すれば10分ぐらいで終わります。

import oracledb
import json
import re
from pypdf import PdfReader
from sentence_transformers import SentenceTransformer

reader = PdfReader("pdf\database-error-messages.pdf")

text = ""
# ora-エラーのみ取得する場合
page_index_start = 2072 # 全量は23から
page_index_end = 4259
for page in reader.pages[page_index_start:page_index_end]:
    text += page.extract_text()

# Oracle接続
conn = oracledb.connect(
    user="rag",
    password="rag",
    dsn="127.0.0.1:1521/FREEPDB1"
)

cursor = conn.cursor()

# Embeddingモデル
model = SentenceTransformer('intfloat/multilingual-e5-small')

blocks = re.split(r'(?=ORA-\d{5})', text)
try:
    error_codes = []
    summarys = []
    causes = []
    actions = []

    for block in blocks:
        if not block.strip():
            continue

        error_code = re.search(r'(ORA-\d{5})', block)
        if error_code:
            error_code = error_code.group(1)
            error_codes.append(error_code)

        summary_match = re.search(
            r'ORA-\d{5}:\s+(.*?)\s+Cause',
            block,
            re.S
        )
        cause_match = re.search(
            r'Cause:\s+(.*?)\s+Action',
            block,
            re.S
        )
        action_match = re.search(
            r'Action:\s+(.*?)(?:Additional Information|$)',
            block,
            re.S
        )

        summary = summary_match.group(1).strip() if summary_match else ""       
        if summary:
            summarys.append(summary)          

        cause = cause_match.group(1).strip() if cause_match else ""        
        if cause:
            causes.append(cause)

        action = action_match.group(1).strip() if action_match else ""
        if action:
            actions.append(action)

    embeddings_summarys = model.encode(summarys).tolist()
    embeddings_causes = model.encode(causes).tolist()
    embeddings_actions = model.encode(actions).tolist()
    # 投入データ
    rows = []
    for i,embedding in enumerate(embeddings_summarys):
        rows.append([
            'database-error-messages',
            error_codes[i],
            "SUMMARY",
            summarys[i],
            json.dumps(embedding)
        ])
    for i,embedding in enumerate(embeddings_causes):
        rows.append([
            'database-error-messages',
            error_codes[i],
            "CAUSE",
            causes[i],
            json.dumps(embedding)
        ])
    for i,embedding in enumerate(embeddings_actions):
        rows.append([
            'database-error-messages',
            error_codes[i],
            "ACTION",
            actions[i],
            json.dumps(embedding)
        ])
    sql = """
        INSERT INTO pdf_documents
        (
            source_file,
            error_code,
            category,
            content,
            embedding
        )
        VALUES
        (
            :1,
            :2,
            :3,
            :4,
            TO_VECTOR(:5)
        )
        """
    cursor.executemany(sql, rows)
    conn.commit()
    print("success")

except Exception as e:
    print("ERROR:", e)

finally:
    cursor.close()
    conn.close()

質問を投げてみます。
これまで質問内容はコード内に直接書いていましたが、実行時に入力させるようにしています。

import oracledb
import json
from sentence_transformers import SentenceTransformer
from ollama import chat

print("==================================================")
print("Oracleのエラーについて質問したいことを入力してください。")
print("==================================================")
query = input('>> ')
print("回答を作成中です。しばらくお待ちください。")

# Embeddingモデル
model = SentenceTransformer('intfloat/multilingual-e5-small')

# Oracle接続
conn = oracledb.connect(
    user="rag",
    password="rag",
    dsn="127.0.0.1:1521/FREEPDB1"
)
cursor = conn.cursor()

query_embedding = model.encode(query).tolist()

query_embedding_json = json.dumps(query_embedding)

sql = """
SELECT ERROR_CODE,CATEGORY,content,
       VECTOR_DISTANCE(
           embedding,
           TO_VECTOR(:1),
           COSINE
       ) distance
FROM pdf_documents
ORDER BY distance
FETCH FIRST 100 ROWS ONLY
"""

cursor.execute(sql, [query_embedding_json])

contexts = []

for row in cursor:
    error_code = row[0]
    category = row[1]
    content = row[2].read()
    distance = row[3]
    
    if distance < 0.3:
        contexts.append(
            f"""
            ERROR_CODE:{error_code}
            CATEGORY:{category}
            CONTENT:
            {content}
            """
        )
       
context = "\n\n".join(contexts)

prompt = f"""
以下の情報だけを利用して回答してください。

{context}

質問:
{query}    
"""

response = chat(
    model="gemma3:4b",
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ]
)
print("==================================================")
print(response["message"]["content"])

出力は下記の通りです。

==================================================
Oracleのエラーについて質問したいことを入力してください。
==================================================
>> UNDO不足
回答を作成中です。しばらくお待ちください。
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
Loading weights: 100%|█████████████████████████████████████████████████████████████████| 199/199 [00:00<00:00, 5251.75it/s]
==================================================
提供されたエラーコードとカテゴリの情報から、UNDO不足に関連するエラーコードと説明を以下にまとめます。

*   **ERROR_CODE: ORA-28344**
    *   **CATEGORY: CAUSE**
    *   **CONTENT:** the specified undo tablespace has no more space available.

*   **ERROR_CODE: ORA-45631**
    *   **CATEGORY: SUMMARY**
    *   **CONTENT:** insufficient disk space

*   **ERROR_CODE: ORA-43910**
    *   **CATEGORY: SUMMARY**
    *   **CONTENT:** number is not a valid undo segment number

*   **ERROR_CODE: ORA-43913**
    *   **CATEGORY: ACTION**
    *   **CONTENT:** check undo$

*   **ERROR_CODE: ORA-43912**
    *   **CATEGORY: SUMMARY**
    *   **CONTENT:** string is not a valid undo segment number

*   **ERROR_CODE: ORA-29738**
    *   **CATEGORY: SUMMARY**
    *   **CONTENT:** no active undo tablespace assigned to instance

*   **ERROR_CODE: ORA-29736**
    *   **CATEGORY: ACTION**
    *   **CONTENT:** lower setting of UNDO_RETENTION or wait a while before reissue command to drop undo tablespace

*   **ERROR_CODE: ORA-28356**
    *   **CATEGORY: CAUSE**
    *   **CONTENT:** The lower limit snapshot expression was below the UNDO_RETENTION limit.

これらのエラーは、UNDO Tablespace に十分な空き容量がないために発生します。

**対処方法の提案 (上記エラーコードから):**

*   **空き容量を確保する:** ディスク容量を増やす、または不要なUNDOデータを含むセグメントを削除して空き容量を確保する。
*   **UNDO_RETENTION パラメータの調整:** UNDO_RETENTION パラメータを下げることで、UNDOの保持期間を短縮し、空き容量を増やす。
*   **セグメントの削除:** 存在しないセグメントや不要なセグメントを削除する(ORA-43910, ORA-43912)。

さらに詳細なトラブルシューティングを行うには、これらのエラーが具体的にどのような状況で発生しているのか、また、UNDO関連のパラメータ設定(UNDO_RETENTION, UNDO_TABLESPACE_LIMIT など)を確認する必要があります。

Oracle Database Error Messages(PDF)のような構造化された技術文書では正規表現での取得も簡単ですが、例えば業務文書では支社や部署ごとにフォーマットやキーワードが異なる場合があります。
上の方でもチラッと書きましたが、そんな時はLLMにJSON文字列等で整形させることもできます。
ただし結構時間がかかるらしく、下記では2ページだけですが5分ぐらいかかりました。

from pypdf import PdfReader
from ollama import chat

reader = PdfReader("pdf\database-error-messages.pdf")

text = ""
# ora-エラーのみ取得する場合
page_index_start = 2072 # 全量は23から
page_index_end = 2073 # 4259
for page in reader.pages[page_index_start:page_index_end]:
    text += page.extract_text()
question = """
この文章から
error_code
category
content
をJSONで返してください。
"""
prompt = f"""
以下の情報だけを利用して回答してください。

{text}

質問:
{question}    
"""

response = chat(
    model="gemma3:4b",
    messages=[
        {
            "role": "user",
            "content": prompt
        }
    ]
)

print(response["message"]["content"])

出力は下記の通りです。

[
  {
    "error_code": "ORA-00000",
    "category": "Normal Completion",
    "content": "normal, successful completion\nCause: Normal exit.\nAction: None"
  },
  {
    "error_code": "ORA-00001",
    "category": "Unique Constraint Violation",
    "content": "unique constraint (string.string) violated\nCause: An UPDATE or INSERT statement attempted to insert a duplicate key.\nAction: Either remove the unique restriction or do not insert the key."
  },
  {
    "error_code": "ORA-00017",
    "category": "Trace Event Setting",
    "content": "session requested to set trace event\nCause: The current session was requested to set a trace event by another session.\nAction: This is used internally; no action is required."
  },
  {
    "error_code": "ORA-00018",
    "category": "Session Limit Exceeded",
    "content": "maximum number of sessions exceeded\nCause: All session state objects are in use.\nAction: Increase the value of the SESSIONS initialization parameter."
  },
  {
    "error_code": "ORA-00019",
    "category": "Session License Limit Exceeded",
    "content": "maximum number of session licenses exceeded\nCause: All licenses are in use.\nAction: Increase the value of the LICENSE MAX SESSIONS initialization parameter."
  },
  {
    "error_code": "ORA-00020",
    "category": "Process Limit Exceeded",
    "content": "maximum number of processes (string) exceeded\nCause: All process state objects are in use.\nAction: Increase the value of the PROCESSES initialization parameter."
  },
  {
    "error_code": "ORA-00021",
    "category": "Session Attachment Issue",
    "content": "session attached to some other process; cannot switch session\nCause: The user session is currently used by others.\nAction: Do not switch to a session attached to some other process."
  },
  {
    "error_code": "ORA-00022",
    "category": "Session Access Denied",
    "content": "invalid session ID; access denied\nCause: Either the session specified does not exist or the caller does not have the privilege to access it.\nAction: Specify a valid session ID that you have privilege to access, that is either you own it or you have the CHANGE_USER privilege."
  },
  {
    "error_code": "ORA-00023",
    "category": "Session Detachment Issue",
    "content": "session references process private memory; cannot detach session\nCause: An attempt was made to detach the current session when it contains references to process private memory.\nAction: (None - this is an internal issue)"
  }
]

長文の要約

ここまでOracleのエラーメッセージばかり取り扱っていますが、日本語の長文を取り込んで要約させてみたいと思います。
青空文庫から適当な長編作品をダウンロードしてきます。
今回はラヴクラフトの「狂気の山脈にて」を採用します。注釈など不要な部分は手動で削っておきます。
チャンクサイズについては大き過ぎても小さ過ぎても性能が落ちるので難しいところです。
章ごととか改行ごとではうまくいきませんでした。
長文の要約に関しては、最初にある程度細かく分割し、各チャンクを要約して、その要約したものを何件かまとめて文字列結合して再度要約して…ということを繰り返すとうまくいくそうです。

今回は中間テーブルを作って中身を確認しながら進めます。

DROP TABLE text_documents;

CREATE TABLE text_documents (
    id NUMBER GENERATED ALWAYS AS IDENTITY,
    source_file VARCHAR2(200),
    line_no NUMBER,
    title VARCHAR2(200),
    author VARCHAR2(200),
    content CLOB,
    embedding VECTOR(384, FLOAT32)
);
CREATE TABLE book_chunk_summaries(
    title VARCHAR2(200),
    chunk_no NUMBER,
    summary CLOB
);
CREATE TABLE book_summaries (
    id NUMBER GENERATED ALWAYS AS IDENTITY,
    title VARCHAR2(200),
    author VARCHAR2(200),
    summary CLOB,
    embedding VECTOR(384, FLOAT32)
);

2000文字ごとにチャンクを分けてtext_documentsに投入します。
青空文庫は1行目にタイトル、2行目に作者名を書いていることが多いのでlines[0].strip()とlines[1].strip()で取得し、残りを本文にしていますが、そうなっていないこともあるので、「ニャルラトホテプ」の作者が「NYARLATHOTEP」になったり「ダゴン」の作者が「DAGON」になったりしますが、今回は気にしないことにします。

import glob
import os
from sentence_transformers import SentenceTransformer
import oracledb
import json

def chunk_text(text, size):
    chunks = []

    for i in range(0, len(text), size):
        chunks.append(text[i:i+size])

    return chunks

def read_all_text_files(folder_path):
    """
    指定フォルダ内(サブフォルダ含む)の全ての .txt ファイルを読み込み、内容を返す。
    """
    if not os.path.isdir(folder_path):
        raise NotADirectoryError(f"指定されたパスはフォルダではありません: {folder_path}")

    # 再帰的に .txt ファイルを取得
    pattern = os.path.join(folder_path, "**", "*.txt")
    file_paths = glob.glob(pattern, recursive=True)

    results = {}
    for file_path in file_paths:
        try:
            with open(file_path, "r", encoding="utf-8") as f:
                results[file_path] = f.read()
        except UnicodeDecodeError:
            # UTF-8 で読めない場合は BOM 付き UTF-8 や Shift_JIS を試す
            try:
                with open(file_path, "r", encoding="utf-8-sig") as f:
                    results[file_path] = f.read()
            except UnicodeDecodeError:
                with open(file_path, "r", encoding="cp932", errors="replace") as f:
                    results[file_path] = f.read()
        except Exception as e:
            print(f"ファイル {file_path} の読み込み中にエラー: {e}")

    return results


if __name__ == "__main__":
    context = ""
    chunks = []
    folder = 'C:\\作業\\txt\\aozorabunko_text-master\\aozorabunko_text-master\\cards\\001699'
    try:
        # Embeddingモデル
        model = SentenceTransformer('intfloat/multilingual-e5-small')

        # Oracle接続
        conn = oracledb.connect(
            user="rag",
            password="rag",
            dsn="127.0.0.1:1521/FREEPDB1"
        )

        cursor = conn.cursor()

        contents = read_all_text_files(folder)
        for file_path, text in contents.items():
            text = "".join(text)
            lines = text.splitlines()
            title = lines[0].strip()
            author = lines[1].strip()
            body = "\n".join(lines[2:])
            chunks = chunk_text(body,2000)

            for i, chunk in enumerate(chunks):
                embedding_text = f"""
                作品名:{title}
                AUTHOR:{author}
                
                {chunk}
                """
                embedding = model.encode(embedding_text).tolist()
                cursor.execute(
                """
                INSERT INTO text_documents
                (
                    source_file,
                    line_no,
                    title,
                    author,
                    content,
                    embedding
                )
                VALUES
                (
                    :1,
                    :2,
                    :3,
                    :4,
                    :5,
                    TO_VECTOR(:6)
                )
                """,
                [
                    'Aozora-Bunko',
                    i,
                    title,
                    author,
                    chunk,
                    json.dumps(embedding)
                ]
                )
        conn.commit() 
        print("success")
    except Exception as e:
        print(f"エラー: {e}")
    finally:
        cursor.close()
        conn.close()

text_documentsに投入したチャンクのうち、「狂気の山脈にて」のcontent(本文)をSELECTします。
CLOB型なので文字列にするため、read()が必要です。
元は2000文字ですが、これを300文字以内で要約させます。
1時間近くかかるので、10件ごとにbook_chunk_summariesにINSERT、COMMITして進捗も表示しています。
対象は55チャンクなので、残り5チャンクを最後にINSERTしています。

from sentence_transformers import SentenceTransformer
from ollama import chat
import oracledb

if __name__ == "__main__":
    context = ""
    chunks = []
    # Embeddingモデル
    model = SentenceTransformer('intfloat/multilingual-e5-small')
    # Oracle接続
    conn = oracledb.connect(
        user="rag",
        password="rag",
        dsn="127.0.0.1:1521/FREEPDB1"
    )
    select_cursor = conn.cursor()
    sql = """
    SELECT content
    FROM text_documents
    WHERE title='狂気の山脈にて'
    ORDER BY id    
    """
    insert_cursor = conn.cursor()
    sql2 = """
        INSERT INTO book_chunk_summaries
        (
            title,
            chunk_no,
            summary
        )
        VALUES
        (
            :1,
            :2,
            :3
        )
        """
    select_cursor.execute(sql)
    chunk_summaries = []
    try:
        i = 0
        for row in select_cursor:
            text = row[0].read()
            
            question = """
            以下の文章を300文字以内で要約してください。
            登場人物名や出来事は省略しないでください。
            """
            prompt = f"""
            以下の情報だけを利用して回答してください。

            {text}

            質問:
            {question}    
            """

            response = chat(
                model="gemma3:4b",
                messages=[
                    {
                        "role": "user",
                        "content": prompt
                    }
                ]
            )

            content = response["message"]["content"]
            chunk_summaries.append(['狂気の山脈にて',i,content])
            i = i + 1
            if i % 10 == 0:
                insert_cursor.executemany(sql2, chunk_summaries)
                conn.commit() 
                chunk_summaries = []
            print(f"chunk={i}, len={len(content)}")
            print(f"INSERT {len(chunk_summaries)} rows")
        # 残り
        if chunk_summaries:
            insert_cursor.executemany(sql2,chunk_summaries)
            conn.commit()
        print("success")
    except Exception as e:
        print(f"エラー: {e}")
    finally:
        select_cursor.close()
        insert_cursor.close()
        conn.close()

要約の中身を確認しておきましょう。

select summary from book_chunk_summaries
order by chunk_no
;

ぱっと見では大体合ってそうです。

"南極への大規模な探査計画に反対する科学者は、その非現実的な野心と捏造の可能性を指摘し、慎重な姿勢を示しています。
彼らは、これまで得られた写真や航空写真が巧妙な遠距離撮影である可能性を疑い、既存の調査方法に疑問を呈しています。


地質学者たちは、新たな掘削装置(ピーボディ型ドリル)を用いた探査を通じて、先カンブリア紀の地層から大量の化石を含む岩石サンプルを得ることを目指していました。
特注の航空機を用いて氷河や山岳地域での調査を行い、地球の初期生物史を解明しようとしていました。
しかし、その活動は日陰的な研究者によるものであり、世論への影響力が限られているため、慎重な判断が求められています。
"
1930年、氷海調査隊はボストン港を出航し、パナマ運河を経由してサモア、タスマニア州ホバートに寄港。
老練な捕鯨者であるダグラス船長とトルフィンセン船長が率いる調査隊は、南極圏へ到達。
その後、ロスが発見したアドミラルティ山地付近で氷山に悩まされつつも、未知の大陸への足掛かりとなるマクマード湾を目指す。
調査隊はエレバス火山の麓に基地を設営する予定であり、驚くほど良好な装備と先人達の経験を参考にしながら、犬、橇、機器類などを備えた状態で航海を進めていた。
 昼間の陸映や蜃気楼などの大気光学現象にも遭遇し、その壮大な調査活動は世界中の注目を集めることなく続いた。

南極への探査隊は、斜光の下、孤絶した山頂で不気味な状況に直面する。
調査隊はエレバス山、ロス島、テラー山などを巡る活動を行い、氷堤の登攀や鉱物の発掘を試みる。
ダンフォースという大学院生がポオに関する知識を豊富に持ち、アーサー・ゴードン・ピムなどの作品に関心を示す。
補給物資、犬、橇など多数の機材を陸揚げし、小型無線機による通信を確立。
氷堤上における拠点整備や飛行機の組み立てを行い、チームの健康状態も良好であったが、悪天候への備えとして、アーカム号での越冬計画も立てていた。
調査期間中に記録された活動内容を新聞にも報道されており、困難な状況下でも調査を進めるべく努力する様子が描かれている。

南極大陸探検隊による氷河調査がこの文章で語られています。
ベアードモア氷河に到達し、ナンセン山上に南方基地を確立しました。
ボーリングと発破作業により、花崗岩や砂岩から多様な化石(羊歯、海藻など)が出現。
粘板岩には奇妙な三角形の痕跡が見つかりました。
同時に、極点上空での観測飛行を行い、蜃気楼のような極地の風景を体験しました。
調査を通じて、南極大陸西側の地域が他の大陸と繋がっている可能性を示唆し、地質学的特徴に関する貴重なデータを収集しました。

南極調査隊は東への移動計画を実行に移し、新たな前進基地を設営するため四機投入した。
当初の仮説が誤ったものの、地質学的な標本採取を目指した。
隊員たちは最高の健康状態を維持し、悪天候にも対応したが、レイクが粘板岩の調査に執念深く取り組むため、大規模な計画変更を余儀なくされた。
彼は極めて古い時代の化石に強い関心を示し、人類未踏の地へと進出したが、その行動は一般大衆の想像力を刺激した。
南極基地側はレイクの過激な動きを抑制し、他の隊員と共に南方基地に留まり、東への移動計画を練りつつ、十分な備えとしてガソリン補給と橇、犬を手配した。
無線通信を通じて情報共有が行われ、レイク隊の調査活動が継続された。

極地探検隊のレイクが、南緯76度15分東経113度10分の巨大山脈を発見。
ヒマラヤ並みの標高で、噴煙を上げる円錐火山や雪のない黒い峰など、驚くべき景観が広がっていた。
地質調査の結果、粘板岩層と規則的な立方体構造の堡塁が見つかり、その起源は謎に包まれていた。
キャロルと共に飛行し、最高峰は約9000メートルから10500メートルと推定された。
隊員たちは興奮を抑えきれず無線で報告を続けたが、天候不良のため本格的な調査は困難な状況だった。

調査隊はレイク率いることで南極探査を進めていた。
極薄の氷床と黒土地帯に新たな基地を設営しつつ、ボーリングや発破など多大な物資を必要とする活動を展開。
東への飛行計画を見直し、氷堤上の犬橇隊との連携を図る中で、調査対象の山脈における地層がジュラ紀以降の砂岩や片岩で構成され、五億年以上前の標本採取に繋がる原始的な地層は確認できないという事態に遭遇。
レイクが急ぎキャンプを設営する中、極寒と強風による困難な状況下で、三角通信を通じて飛行機を南方基地へ派遣し、隊員や自身を乗せて燃料を輸送する計画が合意された。
ピーボディらと共に基地を閉鎖・恒久的なエスキモー村にする準備を進める一方、未知の領域への探索は継続されることになった。

ゲドニー率いる調査隊が砂岩層を掘削した結果、五千万年以上前に熱帯気候だった頃の地下水によって形成された石灰岩空洞を発見しました。
この空洞内には、白亜紀からオルドビス紀までの多様な化石(魚類、軟体動物、珊瑚など)が大量に堆積しており、生命の連続性が三億年もの間維持されていた可能性を示唆します。
初期生命形態の優勢ぶりは極めて特異であり、特にコマンチ紀からの連続性が驚くべき割合で確認されました。
レイクは空洞の古さから更新世の氷河期が外界との交流を断ち切ったと結論付け、今後の調査に備えました。

"六億年前のコマンチ紀において、ファウラー氏が砂岩・石灰岩の破片から、始生代の生物の痕跡を特定。
数学・物理学のアインシュタインに匹敵する意義を持つ発見であり、地球上には十億年以上前の細胞以前にも有機生命のサイクルが存在した可能性を示唆した。


さらに、陸棲・海棲の大型動物における特異な外傷を発見し、鍾乳石を除去して地下探査を拡大。
その後、オレンドルフとワトキンスが未知の樽型化石(過大化した海棲放射相称動物か植物)を発見。
損傷した翼を持つこの化石は、「ネクロノミコン」に登場する旧支配者を想起させるものであり、その存在は極めて原始的であると判断された。
集団発見も行われ、周囲には奇妙な石鹸石の破片が散在していた。

"
"発見された生物は全長2.4mの樽状胴体を持つ原始的な海百合に似た異質な存在である。
五つの膨大部と多数の翼を持ち、その翼は鋸歯状で開口部は呼吸孔として機能する。
管状の肢は計25本に分岐し、先端には眼球のような虹彩を持つガラス質の球を装着している。


胴体下部にはひれ足が付属し、筋肉質な腕も確認された。
この生物の進化は驚くべきものであり、放射相称動物の起源に関する謎めいた側面を持つ。
堆積物は白亜紀終わりから始新世初めのものと推定され、保存状態は奇跡的である。


今回の発見により、広大な研究分野が開かれ、また「旧支配者」などの超太古の生命体との類似性も指摘されている。
標本の運搬作業が今後の課題となっている。

"
"調査隊がマクマード湾での活動を開始し、遺体の剖検を進める中で、極めて異質な標本との遭遇に直面する。
解剖作業は困難を極め、発見された組織の柔軟性と強靭さに驚愕する。


その標本は無機塩類による置換がなく、四千万年前のものであるにも関わらず内部臓器が損傷なく、皮革のような劣化しにくい性質を持っていた。
また、血清のような暗緑色の液体を排泄し、周囲の犬に悪臭と興奮を引き起こすなど、従来の生物学を覆すような存在だった。


この未知の生命体の正体は動物か植物か特定できず、レイクは没頭した結果、海星状基部から管が伸びる奇妙な構造を持ってしまった。
剖検は謎を深めるばかりで、調査隊に新たな疑問と混乱をもたらした。
"
レイクは未知の生物「旧支配者」の解剖を行った。
その神経系と諸臓器は驚くほど複雑で高度に発達しており、五葉型の脳や海洋起源の形態を示す一方で、衰退的な特徴も持ち合わせていた。
解剖後、標本を保護するためテントを設置し、南極の厳しい環境下での作業を進めた。
風雪対策として、壁の強化や犬囲いの新設、飛行機の風よけなどが行われた。
生物の進化について、民俗学者の仮説や先カンブリア紀の化石との比較検討も行われたが、複雑な神経系と衰退的な形態から「旧支配者」という名前を与えた。
作業は緊張感の中で進められ、生物の保護のために様々な対策が講じられた。

"南極でのレイク捜索のため、一行はマクマード湾から五機目の飛行機で出発しました。
雷克の連絡が途絶え、嵐と通信障害により懸念が高まる中、調査隊は迅速な行動に出ました。
シャーマンら水夫と共に、機材や犬、橇などを搭載し、北西へ向かいました。


極めて困難な状況下での長距離飛行に対し、一行は不安を共有しながらも前進しました。
レイクとの接触に失敗し、恐怖と混乱が広がる中、調査隊の結束と決意は揺るぎませんでした。
この四時間半の飛行は、隊員にとって人生を変えるほどの経験となりました。

"
この物語は、レーリッヒが描いた雲と山脈の幻像に魅せられた冒険者の体験を描いている。
彼は、三角屋根を持つ尖塔のような奇妙な地形や、立方体の断片が続く規則性を見出し、それが原初の神話の恐怖と関連していると確信する。
その異様な風景は危険を孕み、乳白色の輝きを増す蜃気楼に幻惑され、キュクロプス式の都市のような非現実的な構造物が見えるまでに悪化していく。
彼はまた、『ネクロノミコン』で知られた民俗学者ウィルマースのイメージにもなり、危険な始生代の怪物を孕んだ世界への接近を避けるようになる。

極地での調査隊は、スコースビーの観察記録に似た蜃気楼の中に、歪んだ円錐やターレットが並ぶ奇妙な建造物を発見する。
未知の山脈や高聳え立つ峰々を前に、レイク隊は南極で氷嵐によりレイク隊全体が壊滅し、十一名死亡、ゲドニー青年が行方不明になったことを報告する。
詳細をぼやかしていることへの容赦も得られず、壊滅の原因となった恐ろしい風について秘密を守り続ける。
飛行機や掘削装置は被害を受け、生物標本は失われ、五芒星形の石鹸石の破片など奇妙な鉱物が見つかるものの、チーム全体が未だに恐怖と当惑の中にいることを示唆している。

ゲドニー探検隊は、雪崩で破壊されたキャンプ地帯で、レイク隊の発見した十四体の生物標本や科学機器などを回収する。
犬は動揺し、奇妙な石鹸石や臭いを嗅ぎ、損傷した標本からレイクの記述が正確であることが判明する。
八体の完全標本は吹き飛ばされていた。
隊員たちは、恐怖の飛行や未踏の領域などに関する情報を慎重に語らず、公衆への影響を考慮し、報告を制限していた。
ダンフォースは「何かを見た」としか言わず謎を残す。
山脈の始生代粘板岩、立方体構造、洞窟口、隘路、そして六千メートル超の高地などの特徴も記録されている。

調査隊は南極で不慮の事故を受け、山脈の反対側へ着陸。
飛行機に不可解な痕跡があったものの、幸いにも異変に気づかれずに基地へ帰還。
隊員の一部が精神的に不安定になり、疑惑と恐怖を抱えながら脱出した。
その後、全機と装備を乗せた船でロス海を目指すが、極地の過酷な環境に苦しみ、未知の時代との遭遇など、異常事態を経験する。
探検を思いとどまらせようとするも、好奇心は依然として強く、調査結果や発見物は秘匿されたままだった。

レイクのキャンプで発見された遺体は、極めて異常な状態だった。
死んだ動物も人間も、恐るべき闘争の結果、酷く切断・分解され、内部臓器を抜き取られていた。
塩が撒かれており、その場所は飛行機用風よけの中で起こされていた。
犬の死体もまた紛失しており、隊員と犬の死は、原因不明の混乱をもたらした。
心象に影響を与えようとする試みがあったものの、状況の恐ろしさが人々を狂気へと駆り立てた。
レイク隊が目撃したことは、信じ難い理由により、混沌とした状況を説明するものとなる可能性があった。

レイク率いる隊員と一頭の犬が、崩壊した囲いの雪上に足跡を残しているところから調査を開始する。
解剖用テントで、実験台に覆われた原初の怪物の部分標本が持ち去られている事実が判明し、六体の埋葬物(不快な臭気を放つ)も発見される。
隊員は失踪しており、解剖器具やガソリン焜炉の痕跡など、混乱した状況を目の当たりにする。
キャンプ周辺では奇妙な機械装置の痕跡や食糧の紛失などが確認され、また五芒星形の雪塚と緑っぽい石鹸石が類似性を示すことが判明。
隊員たちは極寒、悪魔のような山嵐、そして異様な標本群に晒されたことで精神的に不安定になり、スタークウェザー=ムーア調査隊の計画を中断することを決定する。

この記録は、探検隊がレーリッヒ風の山脈を探査する様子を描いています。
ダンフォースとチームは、航空カメラと装備を搭載した飛行機で、標高6900~7200mの山岳経路を飛行し、規則的な立方体や堡塁などの奇妙な構造物を発見します。
キャンプは海抜3500m強の麓にあり、隊員は高度を上げながら薄い大気と寒さに警戒しています。
山腹には風蝕を受けた岩石の層が確認され、その山脈が500万年以上前から存在していたことが示唆されます。
極限状態の中で恐怖と混乱を受け止めながらも、科学への情熱と未知領域への探究心を維持する探査隊の姿が描かれています。

"洞窟の口を中心に、調査隊は奇妙な構造を持つ山塊と洞窟を発見した。
珪岩の始生代の素材は規則正しく、マチュ・ピチュ遺跡に類似しており、キュクロプス式ブロックの印象を与えた。
地質学的な考察から、火山性ではない可能性が示唆された。
洞窟内の石灰岩層の分布は魔法的な成形を連想させ、鍾乳石や石筍が見られない点が謎である。


高度を上げるにつれて、調査隊は氷河の状況や雪崩のリスクを認識し、スコット、シャクルトン、アムンゼン達の遠征と比較した。
稜線を回り込む際、未知の領域への期待感と同時に、山地の気候や自然現象が、異国の詩と禁断の書物のような古き神話的なイメージを喚起する、幽玄で捕らえ所のない雰囲気を感じ取った。
"
高山隘路を登攀する探検家は、絶壁にそびえ立つ巨大な石塔と堡塁の群像を目撃し、畏怖と驚愕に包まれる。
広がる石材の迷宮は、幾何学的な構造を持ち、かつて人類が存在しなかった頃からの痕跡を物語るかのように思える。
蜃気楼や歪みによって異様な光景が強調される中、彼らは自然法則への侵犯、そして古代文明の存在を示唆する場所へとたどり着く。
極寒の地で何百万年もの間存在してきた巨大構造物と、その背後にある謎めいた過去に、探検家たちは理性を失い、世界の屋根(コロナ・ムンディ)という言葉を口にする。
彼らは、現実と幻想の境界線を曖昧にし、人類の起源に対する疑問を投げかけるような、驚愕の光景を目の当たりにしたのである。

広大な領域に点在する、四方八方に建物が立ち並ぶ石の迷宮都市を調査した。
その構造は、巨大な蜂の巣のような形状や円錐形、角錐形が主体で、アーチの技術を用いた複雑な建造物だった。
風化により崩壊が進む一方で、石の橋や窓など、かつての繁栄を示す痕跡が散見された。
広大な刈り跡は過去に大河が流れていたことを示唆し、都市は数百万年前から存在していたことが分かった。
訪問者は、古代文明と人類以前の時代の遺物を目の当たりにし、混乱しながらも詳細な観察を試みた。
その目的は、都市の建設者や、異質な領域に集まった生命体との関係を解明することにあった。

古代都市に関する調査飛行を実施中、主人公たちは広大な荒野に都市を発見し、その規模と特徴(星形広場、石の殿堂、蛇の墓に類似する円錐状記念碑など)を観察。
都市は五十キロメートル以上にわたって続いているものの、工作の跡が少ない未踏の荒れ地に消え失せていた。
大河の跡も霧に覆われた西方へ消えていった。
麓の丘でキャンプを設けた後、探査のため厚い毛皮の飛行服と小型装備(コンパス、カメラ、ハンマーなど)を携え、遺跡や岩石標本などの収集を開始した。

"西の空に聳える巨大な石の迷宮を発見し、内部をウサギ狩りごっこで探索する。
氷河期に建設された星形・五芒星形の堡塁や石組を調査中、忘れられた永劫の時間との繋がりを感じる。
 異常な形状の石の蜃気楼や複雑極まる幾何学的構造が広がり、古代都市の建造物の不規則性、重量感、異質性に驚愕する。
石材の扱いや建築技術には未曽有の異常が見られ、人類が思い描ける範を超えたものだと感じた。

"
"この報告書は、考古学的調査の結果に関するものです。
極めて古い都市遺跡を発見し、その年代を特定しようとしています。
壁面が複雑な彫刻と模様で覆われ、建物内部には中生代の植物や動物の遺物が残されています。
石材の分析から、少なくとも50万年、あるいはそれ以上の歴史を持つ可能性があることが示唆されます。
遺跡の中には、石の床を持つ保存状態の良い部屋も発見され、探索を継続するための準備が進められています。
調査隊は恐怖と不安を感じながらも、科学的探求心を持ち続け、サンプル収集に尽力しています。

"
広大な遺跡の調査中、隊員たちは巨大なホールのような空間を発見し、壁面の彫刻と唐草模様を記録した。
拱道と通路を通って建物内部に進むうち、複雑に入り組んだ部屋のネットワークが明らかになった。
安全のため、紙片を用いた経路探索法を採用し、建物内の構造を把握しようとした。
遺跡は異なった建物同士が連絡を取り合い、氷床の下にも橋が形成されている可能性があり、かつて閉鎖・放棄された痕跡が残されていた。
積雪圧力による特殊な状態が形成され、洪水や氷河ダムの決壊も影響している可能性があると推測された。
隊員たちは「遠い昔に死に絶えた蜂の巣」のような、古く謎めいた空間を彷徨うような感覚を体験した。

巨大な異質な都市を発見した探検隊は、その構造と装飾に圧倒される。
複雑な迷宮のような内部には、五芒星や立方体など多様な形状の部屋が存在し、壁面彫刻の系列が床から天井まで続き、高度な数学的原理に基づいた唐草模様が特徴的だった。
渦巻状の装飾は、人類未曾有の洗練された美学を表現しており、動物や植物の描写も迫真性に富んでいた。
探検隊は、その技術と芸術性が、何百万年もの間存在してきた巨大な建築物であることを理解し、他の種族との精神的な隔たりに気付く。
唐草模様の彫り込みや渦巻装飾には、未知の古代言語が用いられていたことが示唆されている。

古代文字の碑文が施された遺跡は、かつての生活や歴史を重視する先史生物の精神を反映し、大量の情報を含んでいた。
壁面の彫刻には高い窓、戸口、石化した木の厚板、透明な窓板、壁龕などが存在し、失われた機械装置との接続も示唆されていた。
床面は砕石や塵芥で覆われていたが、下の方へ行くにつれて清めに近い場所もあった。
天井や床にはタイルが敷かれていたが、多くは落ちてしまっていた。
長期間の静寂に守られたこの迷宮への入室は、感受性の高い人々を恐ろしい古さと孤絶で圧倒し、キャンプでの未説明恐怖と新たな事実によって驚愕させた。
彫刻の保存状態の良い区画にたどり着いた。

"この文章は、古代の巨大都市と、その建設者である異質な存在に関する記述です。
彼らは、現代人類以前に地球上に存在し、高度な知識と技術を持つ「大いなる古きものども」と呼ばれています。
都市の建造物は200万年前のものとされ、その彫刻には数学や宇宙物理学の原理が応用されており、想像を絶する芸術性を持っています。


研究者たちは、この都市の存在を通して人類以前の地球生命の姿を知り、その謎めいた技術や神話に触れました。
著者は、今後の写真公開を通じて、読者に驚愕の事実を伝えたいと考えています。
"
"この文章は、ミスカトニック大学の調査チームが発見した彫刻を通して、星形頭の生物(古きものども)に関する記録をまとめたものである。
彼らは地球に生命を創り出し、高度な科学技術を持つ存在であったが、機械技術の広範な応用を避け、超自然的な強靭性を持ち合わせた生命体として存在していた。
特に「ショゴス」と呼ばれる物質は、多細胞性の原形質塊として利用され、人為的な奴隷として扱われた。
海中都市の巨大化や建築様式は、彼らの宇宙での活動経験から来たものと考えられる。
彫刻に残された古第三紀の都市の建築様式との共通点も指摘されている。
調査チームは、これらの情報を公式紀要に掲載し、さらに詳細な研究を進める方針である。

"
"古代文明「古きものども」は、深海と陸上で多様な生活を送っていた。
深海では燐光生物を利用し、頭部のプリズム状繊毛で感覚を捉え、複雑な彫刻や著述を行っていた。
陸上では偽足や翼による移動に加え、肉食性で強い生命力を持ち、羊歯植物に似た方法で増殖した。


彼らは家族を形成し、装飾性の高い住居で生活し、技術と芸術において高度な能力を発揮していた。
しかし、更新世の大氷期により異星人との接触が途絶え、特殊な状態の維持ができなくなった。
統治機構は複雑で社会主義的だった可能性もある。
"
"古きものどもの文明は、貨幣として五芒星形のチップを用いた高度な社会だった。
農業、鉱業、製造業に加え、頻繁な旅行と巨大な翼を持つプテロダクティルスによる都市建設が特徴的。
独自の生物(ショゴス、原始脊椎動物)を輸送手段として用いた。


南極海に初期入植し、その後南太平洋での災厄や異種族との戦争を経て文明が変動。
クトゥルーの落とし子どもの存在も確認される。
最終的に南極に中心都市を構え、地球全体に都市が広がり、考古学的調査への大規模な掘削を提案している。

"
このテキストは、古代の異種族「古きものども」と彼らに対抗したショゴスとの戦いを詳細に描写するものであり、その過程における戦闘、技術的特徴、そして新たな侵略者との遭遇について述べています。
古きものどものは分子レベルでの干渉や変身能力を持ち、時空連続体から生み出された存在であり、敵対勢力たるミ=ゴ(厭わしい雪男)との戦いでは敗北を喫し南極に撤退しました。
さらに、クトゥルーの落とし子のような異質な素材からできた怪物との遭遇は、古きものどものはより遥か遠くの宇宙空間にルーツを持つ存在であることを示唆しています。
これらの戦闘と異形生物の出現は、古きものどめの技術的優位性や、彼らが構築した宇宙的な背景を物語る重要な要素となっています。

"この記述は、世界環境の長大な変遷と、それに伴う巨大都市の滅亡に関する記録である。
石炭紀以降、大陸が分裂・移動し、人類以前の世界の古きものどもによる大石造都市や海底都市が描かれている。
特に鮮新世の標本では、アラスカと北米大陸の繋がりなど、現代とは異なる地理的状況が示され、山脈の隆起や自然災害によって都市が破壊されていく様子が記録されている。


ダンフォースと研究者たちは悪夢のレン高原(レン高原)を発見し、そこから鮮新世の家跡を見つける。
この場所は、『ネクロノミコン』の著者すら論じるのを避けた、恐ろしい地域であり、ヒマラヤ山脈との関連も示唆されている。

"
この記録は、南緯七七度東経七〇度から南緯七〇度東経一〇〇度に位置する異様な土地について記述しています。
かつてそこに都市が築かれていましたが、現在は崩壊しており、山脈はその主要な神殿となる場所として崇拝されていました。
山脈には洞窟や回廊が広がり、地底深部への探査を描いた彫刻も存在します。
この大河は、古きものども[#「古きものども」は太字]によって源流から流れを変えられ、都市の建設に影響を与えました。
かつては夥しい数の石橋が架けられていましたが、現在ではその痕跡が残されています。
記録者は、この土地における芸術家達の鋭敏なセンスを評価し、その歴史的背景を理解しようと努めています。

この地域には、百万年前に遡る彫刻が多数存在し、かつてここに暮らしていた人々が過酷な恐怖に囚われていた様子が描かれている。
都市の放棄は、古きものども[#「古きものども」は太字]の希望が滅び去ったこと、大氷期の到来などが原因と推測される。
退廃彫刻には、温暖な場所への移住や地下深部の利用といった変化が見られ、広大な地上都市と暗い深淵との連絡トンネルも建設されていた。
特に深淵は最大の移民先となり、温暖な環境を求めて水中に都市を築いていたことがわかる。

古きものどもは、地上の都市から石材を選び、新たな海中都市を建設した。
彼らは陸上都市と海中都市の間で移動し、南極の海底都市と取引していた。
冬になると、氷による侵略や家畜の減少など、厳しい環境下でショゴスを調整し続けた。
都市は衰退期に入り、彫刻はそれまでの姿を残したままだった。
彼らの存在は、探査によって明らかになったが、その都市のその後や、北の陸地に広がった古きものども自身については不明である。

"南極の洞窟都市に関する調査が進められている。
地質学的な状況から、標本となった個体は三千万年以上前には存在した陸上都市を舞台にしていたことが判明する。
レイクのキャンプから失われた標本の異常な状態や、古代怪物の強靭さなどが明らかになり、研究者たちは深淵の探索に没頭する。
彫刻の縮尺に基づき、急なトンネルを発見し、氷面下の通路へと進むが、限られた時間とバッテリー残量により、調査は中断される。
建物もまた五芒星構造を持ち、儀式的な性格を示す。
 調査チームは深淵への探査を諦めず、近接する最も近いトンネルへ向かおうとしている。

"
この文章は、洞窟探検の様子を描写しています。
主人公たちは暗い迷宮を進み、様々な彫刻を発見し、何度か足を踏み外すなど苦労を重ねます。
二本の懐中電灯を頼りに進むうち、床に残された痕跡に気づき、それが一般的なガソリンの臭いを放っていることに気付きます。
この事実は彼らをさらに不安にさせ、探検を中断せざるを得ない状況へと追い込みました。
以前の恐怖と未知のものに対する疑念が、今やより一層強まっているのです。

永劫の闇に閉ざされた墓所での調査隊は、キャンプの恐怖と足跡、音楽的笛音といった不可解な現象に直面していた。
調査が進むにつれ、彼らは確実な情報を得ることはできず、崩壊が進む遺跡を探索する中で、散乱したキャンプの痕跡を発見した。
缶詰や図書など、全てがレイクのキャンプからのものだった。
特に、キャンプで既に見ていた紙にインクが付着している状況は、調査隊の精神的な耐性を試すものであった。
彼らは予期せぬ道の閉塞に直面し、深淵への探索は失敗に終わった。

この物語は、未知の氷の迷宮を探索する冒険者たちの顛末を描いている。
彼らは、狂気の山脈や衰退期の彫刻といった異様な地形を通り抜け、当初の目的を失いながらも、極端な精神を持つ人々(写真撮影家など)に倣い、進んでいく。
深淵への入口を探し、巨大な円筒状の塔を発見し、その下に氷面以下の層に重要な建築様式が残されている可能性に気づく。
彼らは恐怖のスケッチを頼りに、円環構造に従って進み、すでに先駆者たちが通過したであろう通路を進む。
この旅は、当初の目的を失いながらも、未知への探求心を掻き立てるウサギ狩りのような紙の倹約を繰り返す冒険へと変わっていく。

考古学調査チームが、五千万年前の巨大な円形空間を発見した。
そこは、古代バビロンのジッグラトのような螺旋状の石の斜路を備えた構造物で、壁には壮大な彫刻が施されていた。
斜路は驚くほど保存状態が良く、高さ十八メートル以上の石材が立ち並んでいた。
チームは、この構造物が最古の原始構造物であり、テラスのある大型建築物からのアクセス経路であると結論付け、氷面下でのさらなる調査を行うことにした。
斜路の一部は塞がれていたものの、最近取り除かれた痕跡が見られた。

氷床奥深くから聞こえる嘲笑的な音に気づいた探検隊は、その源が巨大な深淵へのトンネル付近にあると判断した。
凍土に足跡の痕跡を辿りながら調査を進める中、一羽のペンギンの鳴き声がその音源であることに気付く。
この異常な状況に対し、彼らは現実確認のため、氷床の拱道を進んだ。
そこで見つけたのは、明確な足跡だった。
その足跡は北側のトンネルへと繋がっており、彼らの探索をさらに加速させた。

山中のトンネルを発掘中、四角錐状の建造物と巨大なペンギン(オウサマペンギンの種類)を発見。
調査を進めるうちに、未知の巨大種であるアルビノのペンギン三羽組と遭遇し、彼らがかつて古きものどもと共生していたことを知る。
そのペンギンたちは、冬眠のような理由で繁殖地を離れ、深淵へと進退していた。
半球状の巨大な深淵には、原始時代の天球図のような彫刻があり、その入口は直径30メートル、高さ15メートルの拱道で開けていた。

"極地を掘り進むトンネル内、ペンギンたちを先導する探検隊は、暖かさと蒸気が増すにつれ、名状し難い悪臭に遭遇。
粗い石組と渦巻模様の装飾が特徴的な洞窟を進むうち、水平坑道の増加に気づく。
洞窟の奥には、人工的に破壊された壁によって形成された巨大な空間が現れ、きれいに拭き去られた床面は驚くほど滑らかだった。
名状し難い臭気に加え、ぼんやりとした当惑と恐怖を齎す空間で、探検隊は蜂の巣構造部分への到達と、未知の地底環境への不安を深めていく。
洞窟の特異な状態に加え、悪臭は一層刺激的になり、彼らを圧倒する。

"
"坑口が広がる通路で、ペンギンの排泄物の多さに経路が確定した探検隊は、トンネルの壁面彫刻に驚愕する。
より深い区画では、粗雑な肉太い彫刻が急速に退化しており、これまでの美技とは異質だった。
ダンフォースは、これらを既存の彫刻を削り直したものと推測し、パロディ的な数学的伝統に基づくものだと指摘。
さらに、不穏な類似性からローマ様式の混成芸術を想起させる。
調査時間の短縮のため hastily 退出する際にも、装飾の変化を確認するため懐中電灯を向けたが、変化はなかった。
その後、滑らかな床を持つトンネルを抜ける際、前方の磨かれた床に異様な障害物を見つけ、接近する。
その障害物は、レイクのキャンプで掘り起こされた標本と類似しており、暗緑色のプールから見て、最近の状態であることが判明した。

"
"倒れたペンギンの調査中、探索隊は、頭部が欠損した海星状の体と、嫌悪感のある暗緑色の液体を発見した。
その臭いは、1億5千万年前の彫刻を思い起こさせる異様なものだった。
ダンフォースはヒステリックに叫び、古きものどもの粘液状存在を暗示する芸術作品を想起させた。
探索隊員もまた、ショゴスによる首なし死体に似た不気味な彫刻を鑑賞し、その存在を恐れた。
調査の過程で、不定形の原形質が様々な形態を模倣できることが明らかになった。

"
古きものどもの建造物跡地で発見された異様な粘液と死体の状況に、ダンフォースと私は宇宙的恐怖を味わう。
彼らは氷河下での過酷な環境下で、放射相称動物や類人猿など様々な生物と遭遇し、都市の崩壊を見守った。
彼らの知性と忍耐力は驚くべきものであり、その存在が人類の歴史にどのような影響を与えたのかを問いかける。
粘液状の斑点や異音の発生と共に混乱する様子、そして洞窟内の機械人形のような動きなど、物語全体を通して異常な状況が描かれている。

霧深い森の中で追跡者から逃れる主人公たち。
突如響く「テケリ・リ!テケリ・リ!」という不気味な音と、粘液にまみれた原形質の山を目の当たりにし恐怖する。
追跡者は怪我ではなく同族の遺体とスライムに気づき、先ほどまでの混乱は一掃された。
洞窟へと足を進める中で、幻影や盲目のペンギンに惑わされながらも、状況を悪用して追跡者を誘導しようと試みる。
未知の穴の奥地へ続く道で、主人公たちは死都を目指す間も無く、迷いそうになることを恐れ、慎重に進む。

"追跡者から逃亡中に、濃霧の中、ペンギンと出会い運良く正しい道を選んだ。
振り返った刹那、強烈な悪臭を放つ存在が確認され、恐怖に襲われた。
その動機は不明だが、追跡者の本能や、スライムの臭気変化による無意識的な反応が原因である可能性を示唆している。


懐中電灯を使って視覚を惑わせる間に脱出を試みるも、貴重な光を失った。
ダンフォースはヒステリックに呪文を唱え、その様子は恐怖で何よりも一層増幅された。
ニューイングランドの地下鉄駅名を無意味に唱え続けることで、さらなる混乱と不安が生じた。
全体として、強烈な臭気、奇妙な行動、そして逃走という状況が、人物にとって想像を絶する恐怖体験をもたらした。
"
この物語は、未知の惑星での冒険を描いています。
主人公とダンフォースは、巨大な霊廟のある惑星を探検し、古代の石造円筒を登る間、死滅した種族の別れの挨拶が彫られた石像を見かけます。
彼らは酷い疲労感を感じ、西にはより高い石組、東には大山脈が広がっています。
氷霧に覆われた地形で、生命を脅かすような恐怖と出会い、飛行機に乗り込みます。
麓の丘へ下りる急勾配を下った後、巨大な機影を背景にした奇妙な景観が続いていきます。
全体として、未知の惑星での探索、古代文明の痕跡、そして不可解な遭遇によって構成される冒険物語です。

"極寒の地「カダスの凍れる荒野」に位置する禁断の紫色の山脈(恐怖の山脈)の探査を目的とした調査隊が、その恐ろしい姿を目にする。
五百キロメートル以上の距離にあるにも関わらず、その存在は遠く雪原に聳え立ち、ガス状の亡霊しか住めない高みに危険が及ぶ。


調査隊は広大な五芒星形の遺跡を通り抜け、更なる恐怖の対象である大山地の横断飛行を行う。
その道中、異様な絵画や蜂の巣構造を持つ洞窟に遭遇し、類縁の靄に悩まされる。
機体の状態は良好で、高高度での飛行にも対応するが、アイスダストによる幻想的な現象と困難な状況が続いた。

"
ダンフォースは、山脈横断中に奇妙な幻覚に囚われ、古きものどもに関する異常な観念を繰り返し語り出した。
その恐怖の源は、泡立つ雲の中に現れた幻想的な姿だと信じているが、過去に読んだ『ネクロノミコン』の影響も否定しない。
彼の狂言は、狭間での航空機脱出後も続き、二人は秘密厳守の約束を再確認する。
ダンフォースの発する「黒い窖」「洞窟の辺縁」といった言葉は、彼がかつて読んだ奇抜な書物から得たものであり、その内容自体が現実との境界線を曖昧にしている。
その幻覚と読書の影響によって、彼を悩ませ続けているのだ。

"「(*41)」の一瞬の観察で得られた情報に疑念が生じた。
その状況下、彼が繰り返していた言葉は「テケリ・リ!テケリ・リ!」という単純で意味不明な単語の繰り返しだった。
この言葉の発言は、明白な起源から生まれたものであり、その内容自体が奇妙で不条理さを強調している。
簡潔かつ不快な響きを持つ言葉が、独特の状況の中で繰り返される様子が描かれている。

"

book_chunk_summriesの55チャンクを10個ずつまとめて文字列結合し、300文字以内で要約します。
これで300文字以内の要約が6個になるので、最後にこれをまとめて文字列結合して要約します。

from sentence_transformers import SentenceTransformer
from ollama import chat
import oracledb
import json

if __name__ == "__main__":
    try:
        context = ""
        chunks = []
        # Embeddingモデル
        model = SentenceTransformer('intfloat/multilingual-e5-small')
        # Oracle接続
        conn = oracledb.connect(
            user="rag",
            password="rag",
            dsn="127.0.0.1:1521/FREEPDB1"
        )
        select_cursor = conn.cursor()
        select_sql = """
        SELECT summary
        FROM book_chunk_summaries
        WHERE title='狂気の山脈にて'
        ORDER BY chunk_no   
        """
        GROUP_SIZE = 10
        group_summaries = []
        select_cursor.execute(select_sql)
        # タプルを展開して文字列にしておく
        rows = [
            row[0].read()
            for row in select_cursor.fetchall()
        ]
        # 0から最後まで、10ずつ
        for i in range(0, len(rows),GROUP_SIZE):
            merged = "\n".join(
                rows[i:i+10]
            )
            # 要約10件分を結合して要約し、group_summariesにappendする
            question = """
            以下は小説の連続した一部分です。
            登場人物名や出来事は省略しないでください。

            300文字以内で要約してください。
            """
            prompt = f"""
            以下の情報だけを利用して回答してください。

            {merged}

            質問:
            {question}    
            """

            response = chat(
                model="gemma3:4b",
                messages=[
                    {
                        "role": "user",
                        "content": prompt
                    }
                ]
            )
            summary = response["message"]["content"]
            group_summaries.append(summary)
        final_text = "\n".join(group_summaries)
        print("再要約:")
        print(final_text)
        question = """
        以下は同一小説の各部分の要約です。

        これらは別々の物語ではありません。
        一つの作品として統合してください。
        起承転結で説明してください。
        """
        prompt = f"""
        以下の情報だけを利用して回答してください。

        {final_text}

        質問:
        {question}    
        """

        response = chat(
            model="gemma3:4b",
            messages=[
                {
                    "role": "user",
                    "content": prompt
                }
            ]
        )
        final_summary = response["message"]["content"]
        print("最後の要約:")
        print(final_summary)

        insert_cursor = conn.cursor()
        embedding = model.encode(final_summary).tolist()
        insert_cursor.execute(
        """
        INSERT INTO BOOK_SUMMARIES
        (
            title,
            author,
            summary,
            embedding
        )
        VALUES
        (
            :1,
            :2,
            :3,
            TO_VECTOR(:4)
        )
        """,
        [
            '狂気の山脈にて',
            'ラヴクラフト',
            final_summary,
            json.dumps(embedding)
        ]
        )
        conn.commit() 
    except Exception as e:
        print(f"エラー: {e}")
    finally:
        select_cursor.close()
        insert_cursor.close()
        conn.close()

出力は下記の通りです。
海百合状生物は「古のもの」ですが魔導書ネクロノミコンになっているなど、若干ハルシネーションを起こしているようです。

再要約:

南極への大規模探査計画は、非現実的な野心と捏造の可能性を抱えていた。
地質学者たちは先カンブリア紀の化石探索や初期生物史の解明を目指したが、日陰研究者による活動であり世論影響力が限られていた。
ベアードモア氷河でのボーリング調査、レイク率いる新たな山脈発見、ゲドニー氏による五千万年前の石灰岩空洞発掘など、数々の特異な発見が行われた。
特に、全長2.4mの樽状胴体を持つ原始的な海百合のような生物「ネクロノミコン」の発見は、極めて特異で古くからの生命サイクルを示唆した。
 調査隊は困難な状況下で活動を続けたが、情報共有の遅れや大規模計画変更など課題も抱えていた。

南極調査隊がレイク率いることで、雲と山脈の幻像に魅せられた冒険者の体験を追跡する物語です。
マクマード湾から出発した飛行機は嵐と通信障害によりレーキ隊全体が壊滅し、多くの隊員が死亡または行方不明になります。
残された調査隊は、スコースビーの観察記録に似た蜃楼の中に歪んだ建造物を発見し、さらなる混乱を招きます。
遺体や標本を回収する過程で、未知の生物「旧支配者」の複雑な神経系と衰退的な形態が明らかになり、チームは恐怖と精神的混乱に苦しみます。
極寒と悪魔のような山嵐、そして異様な標本の存在下で、科学への情熱と探求心を持った調査隊は、最終的に計画を中断し、発見された情報を秘匿して基地へ帰還します。

洞窟の口を中心に発見された奇妙な山塊と洞窟内には、マチュ・ピチュに類似する珪岩構造が確認され、キュクロプス式ブロックの印象を与えた。
高度を上げるにつれて氷河状況や雪崩リスクが増大し、スコット、シャクルトン、アムンゼンとの遠征と比較された。
広大な石の迷宮都市を発見し、星形広場や円錐状記念碑など特徴的な建造物を確認。
その複雑な構造と、中生代植物・動物の遺物が残る石室は、少なくとも50万年以上の歴史を持つ可能性を示唆した。
氷床下にも橋が形成されている可能性が浮上し、未知の古代文明との接触を試みる探検隊は、恐怖と不安を感じながらも科学的探求心に裏打ちされたサンプル収集に尽力した。
人類以前の地球生命の姿を知ることを目指す彼らの冒険は、現代人類への疑問を投げかけるものとなるだろう。

古代異種族「古きものども」の痕跡を追う探査隊が、レン高原を発見し、鮮新世の家跡と地下都市を発見する。
都市は崩壊し、山脈が神殿として崇拝されていた。
石材を選び海中都市を建設した古きものどものは南極の海底都市と取引していた。
数千年前の彫刻には苦悩や不安が込められており、温暖な環境への移住や深淵との連絡トンネルも存在した。
 洞窟探検中に床に残された痕跡に気づき、それがガソリンの臭いを放っていた。
探索は中断され、主人公たちは恐怖と未知のものに対する疑念を抱えることになった。

調査隊は永劫の墓所、氷の迷宮、そしてバビロニアのような古代遺跡を探索する中で、不可解な現象と崩壊したキャンプの痕跡に遭遇する。
深淵への探索や巨大な円筒状の塔を発見し、原始構造物との関連性が示唆される。
ペンギンの存在から未知の種族との共生が明らかになり、異様な彫刻や粘液状の存在、そして「テケリ・リ!」という不気味な音など、さまざまな異常事態に直面する。
調査員たちは精神的なストレスと恐怖を味わいながら、未知の環境に翻弄され、目的を見失っていく。


極寒の惑星で、ダンフォースと調査隊は禁断の紫山脈探査中、幽霊のような存在が聳え立つ異様な風景に遭遇する。
遺跡や洞窟を探索する間、幻覚に悩まされるダンフォースの発する奇妙な言葉「テケリ・リ!テケリ・リ!」が、事件の不条理さを際立たせる。
飛行機での移動中も、アイスダストによる幻想的な現象や、類縁の靄との遭遇で困難を極める。
広大な五芒星形の遺跡や大山脈の横断など、危険な状況に直面しながらも、彼らは秘密厳守の約束を再確認し、未知の脅威と向き合っていく。

最後の要約:

## 南極探査記:禁断の地、異次元の謎 - 起承転結による統合版

**起:始まりの混乱と最初の発見**

南極への大規模探査計画は、非現実的な野心と捏造の可能性を孕みながら始まった。
地質学者たちは先カンブリア紀の化石探索に挑むも世論の影響は薄く、ベアードモア氷河でのボーリング調査やレイク率いる新たな山脈発見など、特異な事象が続いた。
特に全長2.4mの海百合のような生物「ネクロノミコン」の発見は、古き生命サイクルを暗示するものであった。
しかし、情報共有の遅れや計画変更など課題も抱えながら、調査隊は困難な状況下で活動を続けた。
最初の混乱は、マクマード湾からの飛行機墜落から始まった。
嵐と通信障害によりレイク隊全体が壊滅し、多くの隊員が死亡または行方不明となり、残された調査隊はスコースビーの観察記録に似た蜃楼の中に歪んだ建造物を発見。
その中に未知の生物「旧支配者」の活動が明らかになり、チームを恐怖と精神的混乱に陥れた。


**承:幻影の都市と古代文明の痕跡**

混乱から脱し、調査隊はレイク率いることで、雲と山脈の幻像に魅せられた冒険者の体験を追跡するようになった。
洞窟の口を中心に発見された奇妙な山塊には、マチュ・ピチュに類似する珪岩構造が確認され、「キュクロプス式ブロック」の印象を与え、高度を上げるにつれて氷河状況や雪崩リスクが増大した。
広大な石の迷宮都市を発見し、星形広場や円錐状記念碑など特徴的な建造物を確認。
その複雑な構造と中生代植物・動物の遺物が残る石室は、少なくとも50万年以上の歴史を持つ可能性を示唆した。
氷床下にも橋が形成されている可能性が浮上し、未知の古代文明との接触を試みる探検隊は、恐怖と不安を感じながらも科学的探求心に裏打ちされたサンプル収集に尽力した。


**転:異種族との遭遇と禁断の探査**

調査隊はその後、レン高原を発見し鮮新世の家跡と地下都市を発見した。
この都市を建設した「古きものども」は海中都市と取引していた。
数千年前の彫刻には苦悩や不安が込められており、温暖な環境への移住や深淵との連絡トンネルも存在した。
洞窟探検中に床に残された痕跡に気づき、それがガソリンの臭いを放っていたため探索は中断され、主人公たちは恐怖と未知のものに対する疑念を抱えることになった。
また、ダンフォースと調査隊は紫山脈探査中、幽霊のような存在が聳え立つ異様な風景に遭遇し、「テケリ・リ!テケリ・リ!」という言葉を発する幻覚に悩まされた。


**結:狂騒と秘密の帰還**

永劫の墓所、氷の迷宮、そしてバビロニアのような古代遺跡を探索する中、調査隊は不可解な現象と崩壊したキャンプの痕跡に遭遇し、「テケリ・リ!」という不気味な音、ペンギンの存在から未知の種族との共生が明らかになり、異様な彫刻や粘液状の存在に直面した。
精神的なストレスと恐怖を味わいながら、調査員たちは未知の環境に翻弄され、目的を見失っていく。
極寒の惑星で、ダンフォースと調査隊は禁断の紫山脈探査中、幻覚に悩まされる発する奇妙な言葉「テケリ・リ!テケリ・リ!」が事件の不条理さを際立たせる。
飛行機での移動中も、アイスダストによる幻想的な現象や類縁の靄との遭遇で困難を極めた。
最終的に、恐怖と未解決の謎に包まれて、調査隊は計画を中断し、発見された情報を秘匿して基地へ帰還した。
人類以前の地球生命の姿を知るという探求心も消え去り、彼らの冒険は、現代人類への疑問を投げかけるものとなった。
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?