Pineconeでベクトル検索してみた

ベクトルDBとして有名なPineconeのベクトル検索を試してみました。
今回はメタデータを使ったフィルタリングも使ってみました。
なおエンベディングにはCohereのモデルを使い、実装にはLangChainを活用しています。

事前準備

必要なPythonパッケージをインストールします。

pip install pinecone-client langchain

Pineconeのサイトにて無料でサインアップし、APIキーを取得します。
https://www.pinecone.io/

Cohereを使う場合は下記サイトにてアカウントを作成し、トライアルのAPIキーを取得します。
https://cohere.com/

取得した各APIキーを .env ファイルに保存し、Pythonコードと同じディレクトリに配置します。

インデックス作成とデータ追加

下記コードは新しいインデックスを作成し、3件のデータを追加します。
サンプルデータはWikipediaから引用した文章になります。
ちなみにPineconeで言うインデックスはOracle DBで言うスキーマのような位置づけみたいです。

import os

from dotenv import load_dotenv
load_dotenv()

from pinecone import Pinecone, PodSpec
from langchain.embeddings import CohereEmbeddings
import json

# embedding設定値
COHERE_API_KEY = os.environ['COHERE_API_KEY']
embeddings = CohereEmbeddings(cohere_api_key=COHERE_API_KEY, model="embed-multilingual-v3.0")

# pinecone設定値
PINECONE_API_KEY = os.environ['PINECONE_API_KEY']
pc = Pinecone(api_key=PINECONE_API_KEY)


# index作成
index_name = "sample-index"
pc.create_index(
    name=index_name,
    dimension=1024,
    metric="cosine",
    spec=PodSpec(
        environment="gcp-starter"
    )
)
index = pc.Index(index_name)

# サンプルデータとしてWikipediaから引用した文章を用意 (メタデータにカテゴリ情報を付与)
sample_texts = [{"text": "生成的人工知能モデルは、訓練データの規則性や構造を訓練において学習することで、訓練データに含まれない新しいデータを生成することができる[8][9]。ジェネレーティブAI、ジェネラティブAIともよばれる。", "category": "AI"}, {"text": "富士山(ふじさん)は、静岡県(富士宮市、富士市、裾野市、御殿場市、駿東郡小山町)と山梨県(富士吉田市、南都留郡鳴沢村)に跨る活火山である。", "category": "mountain"}, {"text": "『鬼滅の刃』は、吾峠呼世晴による日本の漫画作品。", "category": "comics"}]

# embedding
for i in range(len(sample_texts)):
    # Cohereの embed-multilingual-v3.0 でエンベディング
    vector = embeddings.embed_query(sample_texts[i]["text"])

    # エンベディング結果をPineconeのインデックスに保存
    index.upsert(
        vectors=[
            {"id":str(i), 
            "values": vector, 
            "metadata": {"text": sample_texts[i]["text"], "category": sample_texts[i]["category"]}
            }],
        namespace='sample-namespace'
    )

ベクトル検索

「観光地」という単語でベクトル検索するコードです。

import os

from dotenv import load_dotenv
load_dotenv()

from pinecone import Pinecone, PodSpec
from langchain.embeddings import CohereEmbeddings
import json

# embedding設定値
COHERE_API_KEY = os.environ['COHERE_API_KEY']
embeddings = CohereEmbeddings(cohere_api_key=COHERE_API_KEY, model="embed-multilingual-v3.0")

# pinecone設定値
PINECONE_API_KEY = os.environ['PINECONE_API_KEY']
pc = Pinecone(api_key=PINECONE_API_KEY)

# 作成済みインデックスを指定
index_name = "sample-index"
index = pc.Index(index_name)

# ベクトル検索
query_text = "観光地"
query_embed = embeddings.embed_query(query_text)

res = index.query(
    vector=query_embed, 
    top_k=3, 
    include_metadata=True, 
    namespace="sample-namespace"
    )
print(res)

結果は以下の通りです。

{'matches': [{'id': '1',
              'metadata': {'category': 'mountain',
                           'text': '富士山(ふじさん)は、静岡県(富士宮市、富士市、裾野市、御殿場市、駿東郡小山町)と山梨県(富士吉田市、南都留郡鳴沢村)に跨る活火山である。'},
              'score': 0.541224539,
              'values': []},
             {'id': '2',
              'metadata': {'category': 'comics',
                           'text': '『鬼滅の刃』は、吾峠呼世晴による日本の漫画作品。'},
              'score': 0.421164662,
              'values': []},
             {'id': '0',
              'metadata': {'category': 'AI',
                           'text': '生成的人工知能モデルは、訓練データの規則性や構造を訓練において学習することで、訓練データに含まれない新しいデータを生成することができる[8][9]。ジェネレーティブAI、ジェネラティブAIともよばれる。'},
              'score': 0.306574225,
              'values': []}],
 'namespace': 'sample-namespace',
 'usage': {'read_units': 6}}

観光地と一番関連のある富士山の紹介文がトップに来ています。

またメタデータを使ったフィルタリングも出来るということで、「AI」カテゴリに絞ってベクトル検索してみます。
検索ワードは「機械学習」にしました。

import os

from dotenv import load_dotenv
load_dotenv()

from pinecone import Pinecone, PodSpec
from langchain.embeddings import CohereEmbeddings
import json

# embedding設定値
COHERE_API_KEY = os.environ['COHERE_API_KEY']
embeddings = CohereEmbeddings(cohere_api_key=COHERE_API_KEY, model="embed-multilingual-v3.0")

# pinecone設定値
PINECONE_API_KEY = os.environ['PINECONE_API_KEY']
pc = Pinecone(api_key=PINECONE_API_KEY)

# 作成済みインデックスを指定
index_name = "sample-index"
index = pc.Index(index_name)

# ベクトル検索
query_text = "機械学習"
query_embed = embeddings.embed_query(query_text)

# filterにメタデータのフィルタリング情報を設定
res = index.query(
    vector=query_embed, 
   filter={
        "category": {"$eq": "AI"}
    }, 
    top_k=3, 
    include_metadata=True, 
    namespace="sample-namespace"
    )
print(res)

結果は以下の通りです。

{'matches': [{'id': '0',
              'metadata': {'category': 'AI',
                           'text': '生成的人工知能モデルは、訓練データの規則性や構造を訓練において学習することで、訓練データに含まれない新しいデータを生成することができる[8][9]。ジェネレーティブAI、ジェネラティブAIともよばれる。'},
              'score': 0.646658897,
              'values': []}],
 'namespace': 'sample-namespace',
 'usage': {'read_units': 6}}

ちゃんとメタデータの「category」が「AI」の文章だけ検索対象になりました。
簡単ですが以上になります。
少しだけ使ってみて、非常に手軽な印象を受けました。

ローカルの生成AIとベクトル・データベースを使ってRAG対応のチャットアプリを作ってみた

LM Studioで立てたローカルの生成AIと、PGVectorを導入したローカルのPostgreSQLを使ってRAG対応したチャットアプリを作ってみました。
チャットアプリはChainlitを活用しました。

事前準備

各コンポーネントの導入について、参考になる記事は以下の通りです。

ベクトルデータの生成

特定フォルダに格納されているPDFファイルをベクトル化し、PostgreSQLに保存します。
PDFからテキストを抽出するのに pypdf を使いますので、事前にインストールしておきます。

$ pip install pypdf

以下はPDFからのテキスト抽出、テキスト分割、ベクトル化、PostgreSQLへの保存を行うコードになります。
読み込ませるPDFですが、今回は下記で公開されているOracle RACのセミナー資料「RAC_Biginners_20240116.pdf」を使いました。
Oracle Database 入門 – Oracle Real Application Clusters【アーキテクチャ詳説編】
このPDFを下記コードと同じ階層に作成した「docs」というフォルダに格納し、コードを実行します。

import os

from dotenv import load_dotenv
load_dotenv()

import psycopg2
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores.pgvector import PGVector
from langchain.embeddings import CohereEmbeddings
from langchain.docstore.document import Document

# embedding設定値
COHERE_API_KEY = os.environ['COHERE_API_KEY']
embeddings = CohereEmbeddings(cohere_api_key=COHERE_API_KEY, model="embed-multilingual-v3.0")

# db接続 設定値
CONNECTION_STRING = os.environ['CONNECTION_STRING']
COLLECTION_NAME = os.environ['COLLECTION_NAME']

store = PGVector(
    collection_name=COLLECTION_NAME,
    connection_string=CONNECTION_STRING,
    embedding_function=embeddings,
)

# データベースへ接続
PSQL_PW = os.environ['PSQL_PW']
conn = psycopg2.connect(
    host="localhost",
    database="postgres",
    user="postgres",
    password=PSQL_PW
)

# カーソルを作成
cur = conn.cursor()

# 保存済みのベクトルデータを削除
cur.execute("TRUNCATE TABLE langchain_pg_embedding;")

# カーソルと接続を閉じます
conn.commit()
cur.close()
conn.close()

# PDFファイルの読み込み
dirname = "docs"
files = []

for filename in os.listdir(dirname):
    full_path = os.path.join(dirname, filename)
    if os.path.isfile(full_path):
        files.append({"name": filename, "path": full_path})

# テキスト抽出と分割、ベクトル化、保存
for file in files:
    # PDF読み込み
    if file["path"].endswith(".pdf"):
        loader = PyPDFLoader(file["path"])
    else:
        raise ValueError(f"Unsupported file format: {file['path']}")

    # テキスト抽出
    pages = loader.load_and_split()

    # テキスト分割
    text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=100)
    docs = text_splitter.split_documents(pages)

    # DBに保存
    for doc in docs:
        store.add_documents([Document(page_content=doc.page_content, metadata={'source': file["path"]})])

ベクトル化のembedding処理はCohereの embed-multilingual-v3.0 を使っています。
こちらは昨年11月に公開された、多言語対応したembedding用のモデルです。

DBにベクトルデータ、本文を保存する最終行の処理において、ソースとなるファイルパスの情報をメタデータに保存しています。
これによって生成AIの回答と一緒に、回答の引用元も提示できるようになります。
生成AIで問題視される引用元が分からない、という課題については、これで対応できます。

ちなみに上記コードは、実行する度にベクトルデータを保存する表をTRUNCATEしていますので、ご注意ください。
動作確認で繰り返し実行する際に、同じデータが重複して保存され、ベクトル検索が遅くなるのを防ぐための処理です。

チャットアプリの作成

チャットアプリはChainlitを使います。
生成AIはLM Studioで起動したVicunaを利用しました。
Vicunaはオープンソースの中でも、日本語の扱いにかなり長けている印象です。
生成AIとベクトル・データベースの連携はLangChainを使います。

import os

from dotenv import load_dotenv
load_dotenv()

from langchain.vectorstores.pgvector import PGVector
from langchain.embeddings import CohereEmbeddings
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.schema.runnable import Runnable
from langchain.schema.runnable.config import RunnableConfig

import chainlit as cl

@cl.on_chat_start
async def on_chat_start():
    # embedding設定値
    COHERE_API_KEY = os.environ['COHERE_API_KEY']
    embeddings = CohereEmbeddings(cohere_api_key=COHERE_API_KEY, model="embed-multilingual-v3.0")

    # db接続 設定値
    CONNECTION_STRING = os.environ['CONNECTION_STRING']
    COLLECTION_NAME = os.environ['COLLECTION_NAME']

    # embedding設定
    store = PGVector(
        collection_name=COLLECTION_NAME,
        connection_string=CONNECTION_STRING,
        embedding_function=embeddings,
    )

    # promptテンプレート作成
    prompt_template_qa = """以下の文脈を利用して、最後の質問に簡潔に答えてください。答えがわからない場合は、わからないと答えてください。
{context}
質問: {question}
回答(日本語):"""

    prompt_qa = PromptTemplate(
            template=prompt_template_qa, 
            input_variables=["context", "question"],
    )
    chain_type_kwargs = {"prompt": prompt_qa} 

    # LLMモデル定義
    model = ChatOpenAI(base_url="http://localhost:1234/v1", api_key="not-needed", streaming=True)

    # RAG設定
    qa = RetrievalQA.from_chain_type(llm=model, chain_type="stuff", retriever=store.as_retriever(search_kwargs={"k": 2}), return_source_documents=True, chain_type_kwargs=chain_type_kwargs)

    # セッション登録
    cl.user_session.set("runnable", qa)

@cl.on_message
async def on_message(message: cl.Message):
    # セッション情報から設定を読み込み
    runnable=cl.user_session.get("runnable")

    # Chainlit設定
    cb = cl.AsyncLangchainCallbackHandler(
        stream_final_answer=True,
        answer_prefix_tokens=["FINAL", "ANSWER"]
    )

    cb.answer_reached=True

    # 回答生成
    res=await runnable.acall(message.content, callbacks=[cb])

    # 情報源の出力
    source=res["source_documents"][0].metadata["source"]
    if source:
        await cl.Message(content=f"\nSource: "+str(str(source))).send()
    else:
        await cl.Message(content=f"\nNo source found").send()

上記コードを「chat_with_rag.py」というファイル名で保存した場合、以下のように実行してChainlitのチャットアプリを起動します。

$ chainlit run chat_with_rag.py

使ってみる

今回は「SCANとは何ですか?」という質問を投げてみました。
するとLangChainのRetrievalQAによって、PostgreSQL内の類似文書が検索され、検索結果が生成AIに連携されています (下記画像の赤枠)。
01
最終的に返ってきた回答は以下になります。
02
赤枠の部分に、引用元となったドキュメントが示されています。
もしハルシネーションを起こしている場合は、引用元のドキュメントを参照し、適宜修正などを行います。
それによって回答精度の向上が図れます。
本検証の回答については、RAGで正確なドキュメントを参照し生成していますので、正しい内容になっています。

おわりに

LM StudioやChainlitのおかげで、割と簡単にRAG対応した生成AIチャットアプリが作れました。
特定フォルダにドキュメントを放り込んでベクトル化しておけば、あとは生成AIを通して検索から情報整理まで出来るので、かなり便利だなと思いました。

LM Studio + LangChain + ChainlitでローカルPCに生成AIのチャットアプリを作ってみた

以前の記事ではLM StudioでローカルPCに立てたLLMに、REST APIでアクセスしてみました。
今回はその仕組を使ってLangChainと連携し、さらにChainlitでお手軽にチャットアプリを作ってみました。

事前準備

Windows PCであればAnaconda Promptを起動し、仮想環境をアクティベートします。

> activate <仮想環境名>

LangChainとChainlitをインストールします。

> pip install langchain
> pip install chainlit

LM StudioのLLMとLangChain経由で連携し、Chainlitでアプリを起動するコードの作成

Chainlitの公式サイトにあるLangChainと連携するチュートリアルを参照し、今回のコードは以下のように実装しました。
LM Studio上のLLM (今回はVicuna) と連携するため、ChatOpenAIメソッドの引数 base_url には、LM Studioで立てたローカルLLMサーバのエンドポイントを指定しています。

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema import StrOutputParser
from langchain.schema.runnable import Runnable
from langchain.schema.runnable.config import RunnableConfig

import chainlit as cl

@cl.on_chat_start
async def on_chat_start():
    model = ChatOpenAI(base_url="http://localhost:1234/v1", api_key="not-needed", streaming=True)
    prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "質問に対して100字で回答してください。",
            ),
            ("human", "{question}"),
        ]
    )
    runnable = prompt | model | StrOutputParser()
    cl.user_session.set("runnable", runnable)

@cl.on_message
async def on_message(message: cl.Message):
    runnable = cl.user_session.get("runnable")  # type: Runnable

    msg = cl.Message(content="")

    async for chunk in runnable.astream(
        {"question": message.content},
        config=RunnableConfig(callbacks=[cl.LangchainCallbackHandler()]),
    ):
        await msg.stream_token(chunk)

    await msg.send()

上記コードを chat_test.py として保存します。

実行してみる

作成したPythonのコードを実行してみます。
Chainlitを起動する際は、以下のように実行します。

> chainlit run chat_test.py -w

するとChainlitのサーバが起動し、以下のようにチャットアプリが使えるようになります。

01

試しに質問してみると、次の通りローカルのLLMと連携して回答してくれました。

02

以上のように、LM Studio + LangChain + Chainlitを使えば、生成AIを使ったチャットアプリがローカル環境で手軽に出来ちゃいました。
生成AIを使ったアプリ開発の環境として使えるかもしれませんね。

pgvectorを使ってPostgreSQL内の既存のリレーショナル表にベクトルデータを格納してみた

PostgreSQLにはベクトルデータを扱えるようにするために、pgvectorという拡張機能があります。
これによって、既存のリレーショナル形式のデータとベクトルデータを同じ表内で利用できます。
ベクトルデータのみ専用のDBを設けて、そちらに格納する方法も考えられますが、
開発効率やDB運用の手間を考えると、同じDB上に集約して使えるほうが良さそうです。
ということで、今回はPostgreSQLを使い、既存のリレーショナル表に対して
pgvectorを使ってベクトルデータを格納する方法を検証してみました。

事前準備

PostgreSQL、およびPython環境の事前準備については以下の記事を参考にしてください。
生成AI (Cohere)+LangChain+Vector Database (PostgreSQL)でRAGを試してみた
上記の「必要なPythonパッケージのインストール」から「PostgreSQLへのホスト名指定でのログインを有効化」までが対象作業です。

環境が整ったら、検証に使う表を作成します。

CREATE TABLE products_v (
    product_id SERIAL PRIMARY KEY,
    product_name VARCHAR(255),
    manufacturer_name VARCHAR(255),
    product_overview TEXT,
    price FLOAT,
    release_date DATE,
    embedding vector(4096)
);

サンプルデータを入れます。

INSERT INTO products_v (product_name, manufacturer_name, product_overview, price, release_date)
    VALUES ('Tiny Tunes Piano', 
        'Playful Minds Co.', 
        'A colorful and musical toy piano that lets toddlers play and learn songs. The piano has 12 keys, each with a different animal sound and a corresponding sticker. The piano also has a mode switch that allows toddlers to choose between animal sounds, musical notes, or songs. The piano comes with a songbook that teaches toddlers how to play 10 popular nursery rhymes.', 
       29.99, 
       '2023-12-01');

INSERT INTO products_v (product_name, manufacturer_name, product_overview, price, release_date)
    VALUES ('Tiny Blocks Castle', 
        'Playful Minds Co.', 
        'A set of 50 wooden blocks in various shapes and colors that toddlers can use to build their own castle. The blocks are easy to stack and connect with magnets. The set also includes a drawbridge, a flag, and two knight figures. The blocks are made of natural and eco-friendly materials and are safe for toddlers to play with.', 
       39.99, 
       '2024-01-15');

INSERT INTO products_v (product_name, manufacturer_name, product_overview, price, release_date)
    VALUES ('Tiny Pals Farm', 
        'Playful Minds Co.', 
        'A fun and interactive toy farm that introduces toddlers to farm animals and their sounds. The farm has a barn, a silo, a tractor, and a fence. The farm also has 10 animal figures, each with a button that plays its sound when pressed. The farm has a sensor that recognizes the animal figures and plays a song or a fact about them when placed on the farm.', 
       49.99, 
       '2024-02-01');

Pythonスクリプトの作成

既存の products_v 表の product_overview 列の値を使い、Cohereのモデルを使ってベクトルデータを生成します。
出来たベクトルデータをUPDATE文で products_v 表の embedding 列に格納します。
ちなみに embedding 列にベクトルデータを格納する際は、CohereのAPIから返ってきた配列のベクトルデータを、NumPy配列に変換して格納する必要があります。

import os
import numpy as np
import psycopg2
from pgvector.psycopg2 import register_vector
from langchain.vectorstores.pgvector import PGVector
from langchain.embeddings import CohereEmbeddings

COHERE_API_KEY = os.environ['COHERE_API_KEY']
embeddings = CohereEmbeddings(cohere_api_key=COHERE_API_KEY)

# データベースへの接続情報を設定します
conn = psycopg2.connect(
    host="localhost",
    database="postgres",
    user="postgres",
    password="postgres"
)

register_vector(conn)

# カーソルを作成します
cur = conn.cursor()

# SQLクエリを実行します
cur.execute("SELECT product_id, product_overview FROM products_v;")

# 結果を取得します
rows = cur.fetchall()

# 各行のproduct_overviewをベクトル化してproducts_vに格納します
for row in rows:
    doc_result = embeddings.embed_documents([row[1]])
    cur.execute('UPDATE products_v SET embedding = %s WHERE product_id = %s;', (np.array(doc_result[0]), row[0]))

# カーソルと接続を閉じます
conn.commit()
cur.close()
conn.close()

上記コードを embedding.py として保存します。

実行結果

コードを実行します。

$ python embedding.py

psqlで products_v 表の embedding 列を確認します。

SELECT product_id, embedding FROM products_v;

 product_id | embedding                                                                              
------------+----------------------------------------------------------------------------------------
          1 | [-0.31567383,-2.484375,0.58935547,2.5546875,-1.5625,0.2590332,2.1933594,1.703125,0.8295 ...
          2 | [1.0498047,-0.9082031,-1.8886719,0.9897461,-2.6992188,-1.2197266,1.921875,1.0507812,-0. ...
          3 | [1.6728516,-0.46484375,-0.9838867,2.4414062,0.55029297,-0.9790039,0.78808594,1.7128906, ...

以上のようにすれば、リレーショナルデータとベクトルデータを同じ表で扱えます。
データモデルに応じて種類の異なるDBを構築・運用する必要がなくなり、開発効率や運用効率の向上に繋がるのではないでしょうか。

LM Studioを使ってローカルPCで稼働させたLLMにOpenAI互換のREST APIで問い合わせてみた

LM Studioというデスクトップ・アプリを使うと、手元のPCで色々なLLMを試せます。
GPUがない、もしくはオンチップのGPUしかない環境でもLLMを動かせるのが特徴です(ただしCPUとメモリはかなり消費します)。
今回はLM Studioを使ってインストールした Vicuna というLLMに対して、REST API経由でチャット機能を試してみました。
ちなみに Vicuna は日本語に対応したオープンソースのLLMで、Llama2ベースのモデルとなっています。
使ってみた感じ、性能は割りと良さげです。
ちなみにREST APIはOpneAI互換となっているため、OpenAI向けのREST APIで使えます。

LM StudioにVicunaをインストール

LM Studioの検索フォームで「vicuna-13b」と検索し、モデルをダウンロードします。
私は「vicuna-13b-v1.5-16k.Q5_K_M.gguf」をダウンロードしました。

01

Vicunaをサーバとして起動

次にサイドメニューの「Local Server」へアクセスし、上部のプルダウンメニューから先程ダウンロードしたLLMを選択します。
選択できたら緑の「Start Server」を押下します。

02

サーバが起動すると「Server logs」に以下のような出力がみられます。

03

ちなみにサーバを起動すると大量にメモリを消費します。
私のPCでは以下のように負荷が掛かり、LM Studioによって約10GBのRAMを消費しています。

04

Python環境の準備

今回はLLMにアクセスする処理をPythonで書きます。
そのためPythonの環境を準備します。
各種パッケージの導入にはAnacondaを使いました。

Anacondaをインストールしたら「Anaconda Prompt」を起動します。

05

本検証で使うPythonの仮想環境を作成します。

> conda create -n gai

作成できたら仮想環境を切り替えます。

> conda activate gai

OpenAIのREST APIを実行するのに必要なパッケージをインストールします。
APIの仕様の対応状況から、openaiのバージョンは0.28未満を指定しています。

> conda install "openai<0.28"

コードの実行

Vicunaのチャット機能を使うコードを実行します。
サンプルコードは以下の通り用意しました。
質問として「Oracle Databaseについて200文字で簡潔に説明してください。」という文章を投げています。

import openai

openai.api_base = "http://localhost:1234/v1" # LLMのローカルサーバへの接続情報
openai.api_key = "" # APIキーは空欄でOK

completion = openai.ChatCompletion.create(
  model="local-model",
  messages=[
    {"role": "user", "content": "Oracle Databaseについて200文字で簡潔に説明してください。"}
  ]
)

print(completion.choices[0].message.content)

以下が実行結果です。

> python chat_test.py
 Oracle Databaseは、世界最大級のリレーショナルデータベース管理システムです。データの永続性や一貫性を保ちながら、高速かつ安全にデータを処理します。クラスタリング技術により、データのバックアップと回復も容易に行えます。企業や組織のデータ管理に広く使用されています。

それっぽい回答を得られました。
ちなみに回答生成中のCPU負荷は以下のようになっていました。
8コア12スレッドありますが、全て100%に張り付いています。
ちなみに私の環境はオンチップのGPUしかありませんが、それも使わない設定にしています。

06

またサーバログには以下のように出力されていました。
APIのリクエスト情報や処理経過、最終結果の情報が表示されています。

07

以上がLM Studioを使ったローカルPCでのLLMサーバの実行と、REST APIによるアクセスの基本例でした。
次はローカルにベクトル・データベースを立てて、RAGも試してみたいと思います。

PostgreSQL (pgvector) とLangChainを使ってベクトルデータを使った類似検索を試してみた

以下の記事で生成AIの回答をカスタマイズするRAGを試してみました。
生成AI (Cohere)+LangChain+Vector Database (PostgreSQL)でRAGを試してみた
そのRAGの仕組みの中では、Vector Databaseに対する類似検索が行われています。
ということで、今回は類似検索がどういうものか確認してみました。

事前準備

環境は冒頭の記事と同じ環境で動作確認しました。
事前準備として引用記事の「ベクトルデータの生成」までをやっておきます。

類似検索

以下がLangChainを使って類似検索するコード例です。

import os
from langchain.vectorstores.pgvector import PGVector
from langchain.embeddings import CohereEmbeddings

## Cohereのembedding機能を使うためにAPI KEYを用意
COHERE_API_KEY = os.environ['COHERE_API_KEY']

## Cohereのembedding機能を使うための設定
embeddings = CohereEmbeddings(cohere_api_key=COHERE_API_KEY)

## PostgreSQLへの接続文字列
CONNECTION_STRING = "postgresql+psycopg2://postgres:postgres@localhost:5432/postgres"

## ベクトルデータを格納するコレクション名を指定
COLLECTION_NAME = "products_v"

## 操作するコレクションを設定
store = PGVector(
    collection_name=COLLECTION_NAME,
    connection_string=CONNECTION_STRING,
    embedding_function=embeddings,
)

## 類似検索するためのクエリ
query = "Which is the toy suitable for play with animals?"

## クエリを使った類似検索の実行
docs_with_score = store.similarity_search_with_score(query)

## 類似検索の結果を標準出力
for doc, score in docs_with_score:
    print("-" * 80)
    print("Score: ", score)
    print(doc.page_content)
    print("-" * 80)

出力結果は以下のようになります。

--------------------------------------------------------------------------------
Score:  0.5081855045682844
product name: Tiny Pals Farm, manufacturer name: Playful Minds Co., product overview: A fun and interactive toy farm that introduces toddlers to farm animals and their sounds. The farm has a barn, a silo, a tractor, and a fence. The farm also has 10 animal figures, each with a button that plays its sound when pressed. The farm has a sensor that recognizes the animal figures and plays a song or a fact about them when placed on the farm., price: 49.99$ , release date: 2024/02/01
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Score:  0.6422464102601833
product name: Tiny Tunes Piano, manufacturer name: Playful Minds Co., product overview: A colorful and musical toy piano that lets toddlers play and learn songs. The piano has 12 keys, each with a different animal sound and a corresponding sticker. The piano also has a mode switch that allows toddlers to choose between animal sounds, musical notes, or songs. The piano comes with a songbook that teaches toddlers how to play 10 popular nursery rhymes., price: 29.99$ , release date: 2023/12/01
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Score:  0.7242044622175883
product name: Tiny Blocks Castle, manufacturer name: Playful Minds Co., product overview: A set of 50 wooden blocks in various shapes and colors that toddlers can use to build their own castle. The blocks areeasy to stack and connect with magnets. The set also includes a drawbridge, a flag, and two knight figures. The blocks are made of natural and eco-friendly materials and are safe for toddlers to play with., price: 39.99$ , release date: 2024/01/15
--------------------------------------------------------------------------------

ベクトルデータとして格納されている3つのレコードが、Scoreと共に返ってきました。
Scoreには、クエリとして投げた “Which is the toy suitable for play with animals?” という文章の意図にどれだけ沿っているか、が数値として表現されています。
数値は2つのベクトルデータ(クエリとVector Databaseに格納されている文書)間のユークリッド距離を表していますので、数値が小さいほど類似した文書となります。
通常の検索であれば、クエリと一致する文字列を含んでいるものが結果として返ってくるはずですので、ここが類似検索と通常の検索の大きな違いですね。

VimプラグインのQFixHowmでTODO管理する際の設定、操作メモ

QFixHowmはEmacsのhowmと同等の機能を、Vim上でプラグインとして提供するものです。
QFixHowmを使うことでローカルにwiki形式で情報を整理したり、予定やTODOを管理できます。
導入方法に関しては過去の記事をご参照ください。
qfixhomwの導入・使い方

これまで一時メモなど簡単な使い方しかしていなかったのですが、この度QFixHowmを使ったTODO管理に本気で取り組んでみました。
その際の設定や操作など主要なTipsをメモしておきます。

まずQFixHowm/QFixGrepの結果表示にはQuickFixウィンドウを使用する設定にしています。

let QFix_UseLocationList = 0

こうしておくと g,q でカレンダー表示しながら g,y や g,t で予定・タスクを一覧表示したときに、以下画像のように良い感じの画面構成になってくれます。
ロケーションリストを使う設定だとカレンダー画面の方に予定一覧が出てしまい、とても使いづらいです。。

sample

また g,y の予定一覧は60日分を表示する設定にしています。
これは経験則的に2ヶ月分の予定が分かれば個人的には十分だからです。

let QFixHowm_ShowSchedule = 60

カテゴリータグも設定しています。
私はページではなく予定やタスク毎にタグを付与しています。

" ※実際はもっと多くのタグを設定しています。
let QFixHowm_UserSwActionLock = ['[ ]', '[:work]', '[:private]']

これでアクションロックが使えるので、[ ] と入力した状態でEnterキーを押すとタグが切り替わります。
さらに以下のようにgotoリンクをどこか適切なページへ書いておいて、タグ毎に予定・タスクを一覧化できるようにもしています。
>>>:<タグ名>
※参考:gotoリンクとは

g,y や g,t による予定・タスク一覧化の際はキャッシュを使いたくないので、キャッシュを無効化しています。
理由はこれらコマンドを使うときは大抵TODO一覧を更新した場合が多いためです。

let QFixHowm_ListReminderCacheTime = 0

TODOを更新した際に最新の状態で予定・タスクを一覧化するとき、デフォルトでは g,ry や g,rt と入力します。
rを入力することに対して少し煩わしさを感じた次第です。。

最後に、g,y や g,t による予定・タスク一覧化で検索する対象ファイルは1ファイルに限定しています。
今後QFixHowm内のページが増えていったときに、検索速度が比例して増加するのを防ぐのが目的です。

"let QFixHowm_ScheduleSearchFile = '<your path to todo file>'

また本設定に伴い、予定・タスクはすべて1ファイルに追記しています。
とは言え行数が増え続けるのは嫌だったので、完了した予定・タスクは別ファイルに移動しています。

ちなみにタスクを追記しているファイルには mW といった形でマークを設定しています。
これで ‘W と入力するだけでタスク一覧ファイルを開け、すぐに予定・タスクを追記できます。

これまでに紹介した設定も含め、QFixHowmに関連する設定は以下のようになっています。

" qfixhome-masterにruntimepathを通す
set runtimepath+=<path to your qfixhowm-master>

" キーマップリーダー
let QFixHowm_Key = 'g'

" howm_dirはファイルを保存したいディレクトリを設定
let howm_dir             = 'C:\howm'
let howm_filename        = '%Y/%m/%Y-%m-%d-%H%M%S.md'
let howm_fileencoding    = 'utf-8'
let howm_fileformat      = 'dos'

" キーコードやマッピングされたキー列が完了するのを待つ時間(ミリ秒)
set timeout timeoutlen=3000 ttimeoutlen=100

" QFixHowmのファイルタイプ
let QFixHowm_FileType = 'markdown'

" タイトル記号を # に変更する
let QFixHowm_Title = '#'

" QuickFixウィンドウでもプレビューや絞り込みを有効化
let QFixWin_EnableMode = 1

" QFixHowm/QFixGrepの結果表示にロケーションリストを使用する/しない
let QFix_UseLocationList = 0

" ファイルパスのバックスラッシュをスラッシュにする
set shellslash

" textwidthの再設定
au Filetype qfix_memo setlocal textwidth=0

" 休日定義ファイル
let QFixHowm_HolidayFile = '<your path to qfixhowm>\qfixhowm-master\misc\holiday\Sche-Hd-0000-00-00-000000.cp932'

"オートリンクでファイルを開く
let QFixHowm_Wiki = 1

" wiki保存場所
let QFixHowm_WikiDir = 'wiki'

let SubWindow_Title = "WikiMenu"

" ,yでの予定表示期間
let QFixHowm_ShowSchedule = 60

" カテゴリタグの定義
let QFixHowm_UserSwActionLock = ['[ ]', '[:work]', '[:private]']

"予定・TODO表示のキャッシュ保持時間(秒)
let QFixHowm_ListReminderCacheTime = 0

"予定・TODOの検索ファイル名指定
"let QFixHowm_ScheduleSearchFile = '<your path to todo file>'

以上です。
久々にVimの設定を見直す良い機会となりました。

Node.jsをインストールして簡単なスクリプトを書いてみる

Node.jsをインストールして簡単なスクリプトを動かしてみました。
今回はWSL上のOracle Linux 9.2で試しました。

Node.jsのインストール

yumからインストールします。

$ sudo yum install nodejs

Oracle Linux 9 BaseOS Latest (x86_64)                                  11 MB/s |  15 MB     00:01
Oracle Linux 9 Application Stream Packages (x86_64)                    17 MB/s |  22 MB     00:01
Last metadata expiration check: 0:00:02 ago on Sun 01 Oct 2023 12:55:48 AM JST.
Dependencies resolved.
======================================================================================================
 Package                 Architecture  Version                             Repository            Size
======================================================================================================
Installing:
 nodejs                  x86_64        1:16.20.1-2.el9_2                   ol9_appstream        123 k
Installing dependencies:
 nodejs-libs             x86_64        1:16.20.1-2.el9_2                   ol9_appstream         14 M
Installing weak dependencies:
 nodejs-docs             noarch        1:16.20.1-2.el9_2                   ol9_appstream        7.8 M
 nodejs-full-i18n        x86_64        1:16.20.1-2.el9_2                   ol9_appstream        8.2 M
 npm                     x86_64        1:8.19.4-1.16.20.1.2.el9_2          ol9_appstream        3.4 M

Transaction Summary
======================================================================================================
Install  5 Packages

Total download size: 34 M
Installed size: 168 M
Downloading Packages:
(1/5): nodejs-16.20.1-2.el9_2.x86_64.rpm                               68 kB/s | 123 kB     00:01
(2/5): nodejs-docs-16.20.1-2.el9_2.noarch.rpm                         2.9 MB/s | 7.8 MB     00:02
(3/5): nodejs-full-i18n-16.20.1-2.el9_2.x86_64.rpm                    2.9 MB/s | 8.2 MB     00:02
(4/5): nodejs-libs-16.20.1-2.el9_2.x86_64.rpm                         4.5 MB/s |  14 MB     00:03
(5/5): npm-8.19.4-1.16.20.1.2.el9_2.x86_64.rpm                        1.4 MB/s | 3.4 MB     00:02
------------------------------------------------------------------------------------------------------
Total                                                                 6.6 MB/s |  34 MB     00:05
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Running scriptlet: npm-1:8.19.4-1.16.20.1.2.el9_2.x86_64                                        1/1
  Preparing        :                                                                              1/1
  Installing       : nodejs-libs-1:16.20.1-2.el9_2.x86_64                                         1/5
  Installing       : nodejs-docs-1:16.20.1-2.el9_2.noarch                                         2/5
  Installing       : nodejs-full-i18n-1:16.20.1-2.el9_2.x86_64                                    3/5
  Installing       : npm-1:8.19.4-1.16.20.1.2.el9_2.x86_64                                        4/5
  Installing       : nodejs-1:16.20.1-2.el9_2.x86_64                                              5/5
  Running scriptlet: nodejs-1:16.20.1-2.el9_2.x86_64                                              5/5
  Verifying        : nodejs-1:16.20.1-2.el9_2.x86_64                                              1/5
  Verifying        : nodejs-docs-1:16.20.1-2.el9_2.noarch                                         2/5
  Verifying        : nodejs-full-i18n-1:16.20.1-2.el9_2.x86_64                                    3/5
  Verifying        : nodejs-libs-1:16.20.1-2.el9_2.x86_64                                         4/5
  Verifying        : npm-1:8.19.4-1.16.20.1.2.el9_2.x86_64                                        5/5

Installed:
  nodejs-1:16.20.1-2.el9_2.x86_64                      nodejs-docs-1:16.20.1-2.el9_2.noarch
  nodejs-full-i18n-1:16.20.1-2.el9_2.x86_64            nodejs-libs-1:16.20.1-2.el9_2.x86_64
  npm-1:8.19.4-1.16.20.1.2.el9_2.x86_64

Complete!

以下バージョンのNode.jsがインストールされました。
とても簡単ですね。

[oracle@DESKTOP-VEE1U89 ~]$ npm --version
8.19.4

JavaScriptをNode.jsで動かす

引数として指定した自然数の階乗を計算するスクリプトを作成します。
JavaScriptモジュールを使うため、拡張子をmjsとした下記2つのファイルを作成しました。

$ cat factorial.mjs
export function factorial(num) {
  if ( num < 0 ) {
    return -1;
  } else if ( num == 0 ) {
    return 1;
  } else {
    return ( num * factorial( num - 1 ) );
  }
}

$ cat printFactorial.mjs
import {factorial} from "./factorial.mjs"

let num = process.argv[2];
let ans = factorial(num);
console.log(`The factorial of ${num} is: ${ans}`);

それではprintFactorial.mjsを実行してみます。

$ node printFactorial.mjs 4
The factorial of 4 is: 24

以上、Node.jsによって簡単にJavaScriptでサーバーサイドスクリプトを実装できました。

Oracle Databaseのバージョン間の違いを一覧化してくれるORAdiffを使ってみた

今月頭にORAdiffというOracle Database向けの公式ツールがリリースされました。
※利用にはOracleアカウントが必要です。
ORAdiff
名前から何となく想像できそうですが、Oracle Databaseのバージョン間の違いを一覧化してくれるツールです。
開発者が投稿している以下のブログでも詳細を確認頂けます。
ORAdiff – Find the differences between two Oracle Database releases
ORAdiff is live – compare two Oracle Database and Patch releases

トップページはこんな感じです。
top
一覧化できる項目がサイドメニューに表示されています。
初期化パラメータや権限関連、各種オブジェクトなどがあります。
ちなみに使える機能はサポート契約のあるアカウントかどうかで変わってきます。

以下は初期化パラメータの差分を表示した例です。
parameters
上段の入力フォームで比較したいソースとターゲットのバージョンを指定します。
メジャーバージョンだけではなく、パッチレベルまで指定して比較できます。
入力すると即座に追加、削除、変更された初期化パラメータの一覧が表示されます。
その他の項目も同じ利用方法となっています。

Oracle Databaseのアップグレードでは新バージョンにおける各種変更点の洗い出しが必要ですが、一般的にはマニュアル等ドキュメントの地道な読み込みや、実機確認により対応していました。
ORAdiffはそういった作業の効率化にお役立ち頂けると考えています。

ちなみにORAdiffはOracleのローコードツールのAPEXで開発されています。
この機会に是非お見知り置きを。

Oracle Database 23cのグラフ向け新機能 – SQLを使ったプロパティ・グラフの作成と問合せ実行を試してみた

Oracle Database 23cからプロパティ・グラフ向けの新機能として、SQLを使ったプロパティ・グラフの作成、問合せの実行が追加されました。
これまでプロパティ・グラフを活用するためには、Oracle Databaseとは別にPGX ServerやClientを用意し、PGQLという言語を使う必要がありました。
23cからはSQLのみでプロパティ・グラフを活用できるようになっており、使い勝手がかなり上がっています。
既存のヒープ表を組み合わせて手軽にプロパティ・グラフを作成&活用できてしまいます。

今回の検証では、以前PGXを使ってプロパティ・グラフを使ってみた↓の記事と同じことを、SQLを使って試してみます。
Oracle Database標準のグラフ機能をGraph Server(PGX)経由で試してみた

事前準備

今回も以前の記事と同様、LiveLabsに掲載されているハンズオンをベースに検証します。
そのため以下の記事のLab 5を参考に、プロパティ・グラフ作成で使うヒープ表の作成、およびデータ挿入を実施します。
Analyze, Query, and Visualize Property Graphs with Oracle Database

Lab 5: Create and Populate Tables

SQLのDDLを使ったプロパティ・グラフの作成

プロパティ・グラフを作成するSQLのDDLは以下の通りです。
PGQLとの違いですが、VERTEX TABLESとEDGE TABLESで名前が被らないよう、双方に登場する表はAS句で別名を付ける必要があります。

SQL> CREATE PROPERTY GRAPH customer_360
  VERTEX TABLES (
    customer
  , account
  , merchant
  )
  EDGE TABLES (
    account AS accounts
      SOURCE KEY(id) REFERENCES account(id)
      DESTINATION KEY (customer_id) REFERENCES customer(id)
      LABEL owned_by PROPERTIES (id)
  , parent_of
      SOURCE KEY(customer_id_parent) REFERENCES customer(id)
      DESTINATION KEY(customer_id_child) REFERENCES customer(id)
  , purchased
      SOURCE KEY(account_id) REFERENCES account(id)
      DESTINATION KEY(merchant_id) REFERENCES merchant(id)
  , transfer
      SOURCE KEY(account_id_from) REFERENCES account(id)
      DESTINATION KEY(account_id_to) REFERENCES account(id)
  );

プロパティ・グラフが作成されました。

SQLによるプロパティ・グラフへの問合せ

こちらも以前の記事と同様「別の人を一人挟んで振込が循環している口座情報を抽出するクエリ」を実行してみます。

SQL> set linesize 1000
SQL> set pagesize 1000
SQL> SELECT * FROM GRAPH_TABLE (customer_360
    MATCH
    (a1 IS account)-[t1 IS transfer]->(a2 IS account)-[t2 IS transfer]->(a1)
    COLUMNS (a1.account_no AS a1_account ,t1.transfer_date AS t1_date ,t1.amount AS t1_amount ,a2.account_no AS a2_account ,t2.transfer_date AS t2_date ,t2.amount AS t2_amount)
);

A1_ACCOUNT           T1_DATE               T1_AMOUNT A2_ACCOUNT           T2_DATE               T2_AMOUNT
-------------------- -------------------- ---------- -------------------- -------------------- ----------
xxx-yyy-202          2018-10-10                  300 xxx-yyy-201          2018-10-05                  200
xxx-yyy-201          2018-10-05                  200 xxx-yyy-202          2018-10-10                  300

雰囲気としてはPGQLの表記と似ていますが、以下のような書き方のルールとなっています。

  • FROM句に GRAPH TABLE と記載し、直後にプロパティ・グラフ名を指定
  • MATCH句に検索したい頂点と辺の組み合わせを定義
  • COLUMNS句に出力して欲しい列名を定義

ちなみにトレース情報を確認したところ、内部では通常のSQLにクエリ変換されていました。

SQL> ALTER SYSTEM FLUSH SHARED_POOL;
SQL> ALTER SESSION SET EVENTS '10053 trace name context forever';

SQL> SELECT * FROM GRAPH_TABLE (customer_360
    MATCH
    (a1 IS account)-[t1 IS transfer]->(a2 IS account)-[t2 IS transfer]->(a1)
    COLUMNS (a1.account_no AS a1_account ,t1.transfer_date AS t1_date ,t1.amount AS t1_amount ,a2.account_no AS a2_account ,t2.transfer_date AS t2_date ,t2.amount AS t2_amount)
);

SQL> ALTER SESSION SET EVENTS '10053 trace name context off';
SQL> SELECT VALUE FROM V$DIAG_INFO WHERE NAME = 'Default Trace File';

VALUE
--------------------------------------------------------------
/opt/oracle/diag/rdbms/free/FREE/trace/FREE_ora_2547121.trc

トレースファイルを確認します。

$ grep -a1 "Final query" /opt/oracle/diag/rdbms/free/FREE/trace/FREE_ora_2547121.trc

kkoadsInitCtx(exit): dsCtx=0x7f1ff886ba50 reason=context initialized prmOrHntEna=0 pqEna=0 dirEna=0 pqEnaReason=Not PQ or object not large enough misColStatsEna=1 misColStatsEnaReason=ok
Final query after transformations:******* UNPARSED QUERY IS *******
SELECT "A1"."ACCOUNT_NO" "A1_ACCOUNT","T1"."TRANSFER_DATE" "T1_DATE","T1"."AMOUNT" "T1_AMOUNT","A2"."ACCOUNT_NO" "A2_ACCOUNT","T2"."TRANSFER_DATE" "T2_DATE","T2"."AMOUNT" "T2_AMOUNT" FROM "TEST"."ACCOUNT" "A1","TEST"."TRANSFER" "T1","TEST"."ACCOUNT" "A2","TEST"."TRANSFER" "T2" WHERE "A1"."ID"="T1"."ACCOUNT_ID_FROM" AND "A2"."ID"="T1"."ACCOUNT_ID_TO" AND "A2"."ID"="T2"."ACCOUNT_ID_FROM" AND "A1"."ID"="T2"."ACCOUNT_ID_TO"

上記のようなSQLを書くのと比べたら、プロパティ・グラフ向けのSQLで表現した方が直感的にコーディングでき、かつ後から読んだときに理解しやすいですね。

実行計画は以下のようになっていました。
参照するヒープ表の規模によっては、パフォーマンス面でIn-Memoryと組み合わせるのもありですかね…

SQL> set autotrace traceonly
SQL> SELECT * FROM GRAPH_TABLE (customer_360
    MATCH
    (a1 IS account)-[t1 IS transfer]->(a2 IS account)-[t2 IS transfer]->(a1)
    COLUMNS (a1.account_no AS a1_account ,t1.transfer_date AS t1_date ,t1.amount AS t1_amount ,a2.account_no AS a2_account ,t2.transfer_date AS t2_date ,t2.amount AS t2_amount)
);

実行計画
----------------------------------------------------------
Plan hash value: 1320299673

---------------------------------------------------------------------------------
| Id  | Operation            | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |          |    11 |  1672 |    12   (0)| 00:00:01 |
|*  1 |  HASH JOIN           |          |    11 |  1672 |    12   (0)| 00:00:01 |
|*  2 |   HASH JOIN          |          |     8 |   808 |     9   (0)| 00:00:01 |
|*  3 |    HASH JOIN         |          |     8 |   608 |     6   (0)| 00:00:01 |
|   4 |     TABLE ACCESS FULL| ACCOUNT  |     6 |   150 |     3   (0)| 00:00:01 |
|   5 |     TABLE ACCESS FULL| TRANSFER |     8 |   408 |     3   (0)| 00:00:01 |
|   6 |    TABLE ACCESS FULL | ACCOUNT  |     6 |   150 |     3   (0)| 00:00:01 |
|   7 |   TABLE ACCESS FULL  | TRANSFER |     8 |   408 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("A2"."ID"="T2"."ACCOUNT_ID_FROM" AND
              "A1"."ID"="T2"."ACCOUNT_ID_TO")
   2 - access("A2"."ID"="T1"."ACCOUNT_ID_TO")
   3 - access("A1"."ID"="T1"."ACCOUNT_ID_FROM")

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)
   - this is an adaptive plan


統計
----------------------------------------------------------
          0  recursive calls
          0  db block gets
         29  consistent gets
          0  physical reads
          0  redo size
       1185  bytes sent via SQL*Net to client
        108  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          2  rows processed

簡単ですが以上となります。
21cまでと比べ、手軽にプロパティ・グラフが使えるのを実感できました。