白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

Facadeパターンはファサードなんだろうか

こんにちは。
ぼちぼち、新年度の足音が聞こえてきますね。

さて、タイトルなんですが、前からずっともやもやしてることです。
別にFacadeパターンをやめろとか名前を変えろとかそういう話じゃないんですが、なんか自分の思うファサードとちょっと違うんだよなって思ってる話。

ファサードってなんだ

ja.wikipedia.org

例によってWikipediaを見ていくんですが、「建築物正面のデザインを指す語句」とあります。
建築においての「正面」は基本的に町(都市)や街路に対しての正面と解釈するのが良いかと思います。
複数面のファサードを持つ場合もありますし、極端な地下に埋まっている構造物のようにファサードを持たないものもあります。(地下は必ずファサードを持たないかというと、駅ビルの地下街に対するファサードとかまぁそういうのはあるとは思いますが)

Facadeパターンってなんだ

ja.wikipedia.org

FacadeパターンはおなじみGoFデザインパターンの一つです。

異なるサブシステムを単純な操作だけを持ったFacadeクラスで結び、サブシステム間の独立性を高める事を目的とする。

とありますね。

Facade(ファサード)とは「建物の正面」を意味する。

ともあり、建築用語としてのファサードを由来としていそうなことがわかります。

余談ですが、そもそもGoFデザインパターン自体がクリストファー・アレグザンダーという建築家が提唱したパタン・ランゲージという理論から発想を得ています。(この本めちゃくちゃ高いですが、なかなか面白いです)

微妙なズレ

個人的な感覚としては建築という3次元の構造物をとある方向(意図的に見せようとしている方向)から2次元の面として捉えたものがファサードだと思っています。
それこそハリボテのようにファサードを独立した構造物のように作ることもできなくはないですが、映画のセットでもなければあんまり一般的ではないかなと…。(都市によってはファサードに統一感を求めるため、ファサードと背後の本来の構造物の様式が異なるなんてこともありますが)

一方でFacadeパターンではFacadeクラスと言っているように、ファサード自体を独立したモノとしてみなすことが一般的です。
裏側にある機能にアクセスするための受付や窓口的な役割です。面というよりは点という感じですね。
おそらくオフィスビルモデリングしてFacadeパターンを適用してくださいと言ったら、受付にあたる機能をFacadeクラスとして置くのではないでしょうか。

もう一回振り返ってみますが、ファサードは「建築物正面のデザインを指す語句」です。機能ではなくデザインです。なので特に集約するというニュアンスはありません。
「正面」という言葉から「見せ方」をソフトウェアの概念に落とし込んだらそうなったのかもしれないですが、だとするとインタフェースのほうが近くないですかね。
facadeはフランス語の顔っていう意味なので、英語のfaceと同義です。interfaceってまさにfaceって付いてるし。
じゃあ今のFacadeパターンは何なのっていうと、Reception(受付)パターン…とかですかね。

このズレが「まぁそりゃそうなんだけどさ」という話なのか、「いやそういう捉え方じゃないんだよ」という話なのかはよくわからないです。
いやまぁ、一般的なパターンなんで使いますけどね…。

TypeScriptでLangChainを使ってみる その3 RAGパターン編

こんばんは。
最近Alexaの耳が遠くなってる気がするんですが、気のせいでしょうか。

前回は検索編でしたが、今回は応用としてRAGパターン編です。
shironeko.hateblo.jp

引き続きコードはGitHubにあるので良かったらご覧ください。
github.com

データの収集と登録

参照するデータがないとRAGにならないので、とりあえずデータを集めて保存することにします。
今回はサンプルとしてWikipedia日本の記念日一覧 - Wikipediaというページをデータソースにしてみます。

汎用性もへったくれもないDocumentLoaderですが、まぁサンプルデータ集めるだけなんでね…。

class AnniversaryLoader extends BaseDocumentLoader {
    async load(): Promise<Document[]> {

        const wikipediaQuery = new WikipediaQueryRun({
            baseUrl: "https://ja.wikipedia.org/w/api.php"
        });

        const result = await wikipediaQuery.content("日本の記念日一覧");
        return [new Document({ pageContent: result })];
    }
}

そしてこのままだと1つのでかいドキュメントになってしまうのでいい感じに分割します。分割はTextSplitterですよね。
いやーなんとも雑なコードです。ページのレイアウトが変わったら一発でアウトですが、まぁいいでしょう。

class AnniversaryTextSplitter extends TextSplitter {
    splitText(text: string): Promise<string[]> {

        const lines = text.split("\n");

        let title = "";
        let day = "";
        let anniversaries: string[] = [];

        for (const line of lines) {
            if (line.match(/^== (.+) ==$/)) {
                title = line.split(" ")[1].trim();
            } else if (title.match(/^\d+月$/) && line.match(/^\d+日.+/)) {
                day = line.split(" ")[0];
                anniversaries.push(...line.split(" ")[2].split("、").map((a: string) => `${title}${day} : ${a}`));
            }
        }

        return Promise.resolve(anniversaries);
    }
}

で、こんな感じでChromaDBにベクトル化して登録します。

const embeddings = new OpenAIEmbeddings();
const vectorStore = new Chroma(embeddings, {
    url: process.env.CHROMADB_URL,
    collectionName: "anniversary"
});

const loader = new AnniversaryLoader();
const docs = await loader.loadAndSplit(new AnniversaryTextSplitter());

await vectorStore.addDocuments(docs);

これで記念日の情報がベクトル検索できるようになりました。
ちなみにですが、「今日の東京の天気」みたいなリアルタイムの情報は基本的にはVectorStoreに保存したりせずに直接APIとかを叩くのが良いかと思います。
リクエスト回数が多い場合などにはキャッシュとして保存するのはもちろんありですが。

さくっとRAGパターン

ここまでで構成する要素は一通り触ってきてるのであとは組み合わせるだけです。
今回はちょっとテスタブルを意識して依存性は引数で渡すようにしてみたので、Retriever・LLM・Promptを取得する関数を定義しておきます。

function getRetriever(): BaseRetriever {

    const embeddings = new OpenAIEmbeddings();
    const chroma = new Chroma(embeddings, {
        url: process.env.CHROMADB_URL,
        collectionName: "anniversary"
    });

    return chroma.asRetriever();
}

function getLLM(): BaseChatModel {
    return new ChatGoogleGenerativeAI();
}

function getChatTemplate(): ChatPromptTemplate {
    return ChatPromptTemplate.fromMessages([
        SystemMessagePromptTemplate.fromTemplate(`コンテキストに沿ってユーザの入力に回答してください。複数ある場合はすべて回答してください。
----------
{context}`),
        HumanMessagePromptTemplate.fromTemplate("{input}")
    ]);
}

で、コンソールから入力を受け取りたいのでこうします。連続したい場合は無限ループで回したりなどお好みで。

const reader = createInterface({ 
    input: stdin, 
    output: stdout
});

const question = await reader.question("記念日について質問をどうぞ:");
reader.close();

入力待ちになりますね。

> langchainsample@1.0.0 rag
> ts-node rag_execute.ts

記念日について質問をどうぞ:
順番に

まずは普通にRetrieverで検索して、LLMChainで回答させてみましょう。
シグネチャがないとややこしいかもしれないので、一旦関数定義ごと。入力の他にRetriever・LLM・Promptを引数で渡しています。

async function rag1(question: string, retriever: BaseRetriever, llm: BaseChatModel, chatTemplate: ChatPromptTemplate) {

    const docs = await retriever.getRelevantDocuments(question);
    const context = docs.map(d => d.pageContent).join("\n");

    const llmChain = new LLMChain({ prompt: chatTemplate, llm: llm });
    const result = await llmChain.invoke({ context, input: question });
    console.log(result.text);
}

ちゃんと答えてくれている気がします。でも元のページだと「税理士記念日」っていうのもあるんだけどな…。

記念日について質問をどうぞ:2/23はなんの日ですか?
* ふろしきの日
* 富士山の日  
RetrievalQAChain

で、まぁこういうパターンってよく使うよねってことで実はRetrievalQAChainというChainが元々用意されています。
inputKeyやoutputKeyは時々出てくるんですが、パーツを組み合わせるときにこのキーの値を使うよみたいな宣言です。
複数のデータを渡した際にその内のどれを使うかなどを指定します。RetrievalQAChainだとデフォは「query」なのでテンプレートに合わせて「input」にしておきます。

async function rag2(question: string, retriever: BaseRetriever, llm: BaseChatModel) {

    const chain = RetrievalQAChain.fromLLM(llm, retriever, { inputKey: "input" });
    const result = await chain.invoke({ input: question });
    console.log(result.text);
}

あってはいるんですが、2/22は「猫の日」「食器洗い乾燥機の日」「忍者の日」「乃木坂46の日」「竹島の日」「行政書士記念日」らしいので淡白ですね。
実はこれ引数を見るとわかるのですが、テンプレートを渡さずにLangChainのテンプレートに任せています。

記念日について質問をどうぞ:2/22の記念日を教えて
2/22の記念日は「忍者の日」です。

llmのverboseをtrueにするとプロンプトが確認できるので見てみると、内容的には似たようなことが書いてありますが、すべて出力しろって言うニュアンスはないですね。
(ところで2/22の記念日他にもあるのに2/28のエッセイ記念日が混じってきてるのはなんでなの…)

()
        "kwargs": {
          "content": "Use the following pieces of context to answer the users question. \nIf you don't know the answer, just say that you don't know, don't try to make up an answer.\n----------------\n2月22日 : 竹島の日\n\n2月22日 : 猫の日\n\n2月28日 : エッセイ記念日\n\n2月22日 : 忍者の日",
          "additional_kwargs": {}
        }
()
テンプレートありRetrievalQAChain

テンプレートを利用してRetrievalQAChainを使うにはこんな感じです。

async function rag3(question: string, retriever: BaseRetriever, llm: BaseChatModel, chatTemplate: ChatPromptTemplate) {

    const chain = RetrievalQAChain.fromLLM(llm, retriever, { inputKey: "input", prompt: chatTemplate });
    const result = await chain.invoke({ input: question });
    console.log(result.text);
}

ついでにRetriverが取ってくるドキュメントの件数が5件だと足りなそうなので、10件にしておきます。asRetrieverの引数に件数を指定するだけです。

function getRetriever(): BaseRetriever {

    const embeddings = new OpenAIEmbeddings();
    const chroma = new Chroma(embeddings, {
        url: process.env.CHROMADB_URL,
        collectionName: "anniversary"
    });

    return chroma.asRetriever(10);
}

うーん…良くなってはいるけど全部は教えてくれないですね…。この辺はプロンプトの調整次第というところでしょうか。

記念日について質問をどうぞ:2/22の記念日を教えて
2/22の記念日は以下の通りです。

・竹島の日
・猫の日
・忍者の日
・乃木坂46の日
StuffDocumentsChainとRetrievalChain

LangChainのドキュメントを見ているとRetrievalQAChainはレガシーっぽいです。(なんならConversationalRetrievalQAChainはあるけどRetrievalQAChainは書いてすらない)
js.langchain.com

createStuffDocumentsChainとcreateRetrievalChainを使うのがいい感じなんでしょうか。

async function rag4(question: string, retriever: BaseRetriever, llm: BaseChatModel, chatTemplate: ChatPromptTemplate) {

    const documentChain = await createStuffDocumentsChain({ prompt: chatTemplate, llm: llm });
    const retrievalChain = await createRetrievalChain({combineDocsChain: documentChain, retriever: retriever});

    const result = await retrievalChain.invoke({ input: question });
    console.log(result.answer);
}

慣れればパズル

慣れてくるとコードは割と短いんですが、パラメタとかに何を設定するのかがよくわからなくて結局LangChainの中を毎回見てて時間がかかる…。
まぁいい勉強にはなるんですけどね。

いったん目的としていたRAGパターンを試すことができました。
うーん、次はAgentsに手を出すか…LCEL Chainsの書き方ももうちょっと整理したい気もする…。

TypeScriptでLangChainを使ってみる その2 検索編

こんばんは。
無印で売り切れになってたカレンダーが復活してたので、買って帰ったら4月始まりでした。

前回は基礎編でしたが、今回は検索編です。
shironeko.hateblo.jp

RAGパターンもそうですが、結局どこかからデータを集めたりLLMに渡したりしないと独自データは利用できないので、このあたりはとても大事です。
今回のコードも引き続きGitHubに公開しています。
ちなみに自動テストで書くようにしたんですが、langchainjsがESMじゃないと動かなかったりするものがあってだいぶ混乱しました。(結局スキップしたけど…)
github.com

データの読み込み(DocumentLoader)

まずは簡単にCSVファイルを読み込んでみます。
基本的にLangChainではDocumentという型でデータを扱うので、任意のデータソースからドキュメントを読み込むということでDocumentLoaderという名前です。(たぶん)

こんな感じのCSVを読み込むとして、

都道府県,都道府県(ローマ字),県庁所在地,県庁所在地(ローマ字)
北海道,hokkaido,札幌市,sapporo
青森県,aomori,青森市,aomori
岩手県,iwate,盛岡市,morioka
宮城県,miyagi,仙台市,sendai

CSVLoaderというクラスを使ってこんな感じのコードになります。

const loader = new CSVLoader("./sample_data/sample.csv");
const docs = await loader.load();
console.log(docs);

結果はこんな感じ。デフォだと「見出し: データ」を改行で繋いで1行を1つのデータにしてくれるようです。

[
  Document {
    pageContent: '都道府県: 北海道\n都道府県(ローマ字): hokkaido\n県庁所在地: 札幌市\n県庁所在地(ローマ字): sapporo',
    metadata: { source: './sample_data/sample.csv', line: 1 }
  },
  Document {
    pageContent: '都道府県: 青森県\n都道府県(ローマ字): aomori\n県庁所在地: 青森市\n県庁所在地(ローマ字): aomori',
    metadata: { source: './sample_data/sample.csv', line: 2 }
  },
  Document {
    pageContent: '都道府県: 岩手県\n都道府県(ローマ字): iwate\n県庁所在地: 盛岡市\n県庁所在地(ローマ字): morioka',
    metadata: { source: './sample_data/sample.csv', line: 3 }
  },
  Document {
    pageContent: '都道府県: 宮城県\n都道府県(ローマ字): miyagi\n県庁所在地: 仙台市\n県庁所在地(ローマ字): sendai',
    metadata: { source: './sample_data/sample.csv', line: 4 }
  }
]

カラムの設定をすると、

const loader = new CSVLoader("./sample_data/sample.csv", { column: "都道府県" });
const docs = await loader.load();
console.log(docs);

そのカラムをコンテンツにしてくれるみたい。

[
  Document {
    pageContent: '北海道',
    metadata: { source: './sample_data/sample.csv', line: 1 }
  },
  Document {
    pageContent: '青森県',
    metadata: { source: './sample_data/sample.csv', line: 2 }
  },
  Document {
    pageContent: '岩手県',
    metadata: { source: './sample_data/sample.csv', line: 3 }
  },
  Document {
    pageContent: '宮城県',
    metadata: { source: './sample_data/sample.csv', line: 4 }
  }
]

自分で作る場合にはBaseDocumentLoaderを継承します。例えばデータを読み込む関数を指定する場合だとこんな感じ。

type Load = () => Promise<string>;
export class MyDocumentLoader extends BaseDocumentLoader {
    
    readonly loadFunction: Load;
    
    constructor(loadFunction: Load) {
        super();
        this.loadFunction = loadFunction;
    }

    async load(): Promise<Document[]> {
        const text = await this.loadFunction();
        return text.split("\n").map((line, i) => new Document({ 
            pageContent: line,
            metadata: { lineNumber: i + 1 }
        }));
    }
}

で、実行すると、

const func = async () => `今日は
朝から
とても
いい天気なので
散歩に
行ったよ`;
const loader = new MyDocumentLoader(func);
const docs = await loader.load();
console.log(docs);

こんな感じです。

[
  Document { pageContent: '今日は', metadata: { lineNumber: 1 } },
  Document { pageContent: '朝から', metadata: { lineNumber: 2 } },
  Document { pageContent: 'とても', metadata: { lineNumber: 3 } },
  Document { pageContent: 'いい天気なので', metadata: { lineNumber: 4 } },
  Document { pageContent: '散歩に', metadata: { lineNumber: 5 } },
  Document { pageContent: '行ったよ', metadata: { lineNumber: 6 } }
]

テキストの分割(TextSplitter)

例えばWebページやPDFなんかを1つのデータを読み込むと、とても大きなサイズになってしまいます。
ということはLLMにそのまま渡してしまったりした日にはtoken数がものすごいことになります。
これはなかなかによろしくないので、長いテキストを適度なサイズのドキュメントに分割する仕組みがあると便利そうです。

CharacterTextSplitterを利用すると区切り文字を指定した上でドキュメントに分割することができます。

const text = "吾輩は猫である。名前はまだない。どこで生れたか頓と見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。";
const splitter = new CharacterTextSplitter({ 
    separator: "。", // 区切り文字
    chunkSize: 5,    // チャンクの文字数
    chunkOverlap: 0  // チャンクのオーバーラップ
});
const result = await splitter.createDocuments([text]);
console.log(result);

区切り文字自体は含まれないみたいですね。

[
  Document { pageContent: '吾輩は猫である', metadata: { loc: [Object] } },
  Document { pageContent: '名前はまだない', metadata: { loc: [Object] } },
  Document {
    pageContent: 'どこで生れたか頓と見当がつかぬ',
    metadata: { loc: [Object] }
  },
  Document {
    pageContent: '何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している',
    metadata: { loc: [Object] }
  }
]

チャンクサイズを大きくすると、ドキュメントが過度に小さく分割されるのを防げそうです。

const text = "吾輩は猫である。名前はまだない。どこで生れたか頓と見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。";
const splitter = new CharacterTextSplitter({ 
    separator: "。", // 区切り文字
    chunkSize: 30,    // チャンクの文字数(これを超えない限りは区切り文字があっても1つになるみたい)
    chunkOverlap: 10  // チャンクのオーバーラップ(効いてる気がしない)
});
const result = await splitter.createDocuments([text]);
console.log(result);

30文字になるまでちゃんと1つになってる。

[
  Document {
    pageContent: '吾輩は猫である。名前はまだない。どこで生れたか頓と見当がつかぬ',
    metadata: { loc: [Object] }
  },
  Document {
    pageContent: '何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している',
    metadata: { loc: [Object] }
  }
]

RecursiveCharacterTextSplitterは区切り文字を使わずにサイズで分割します。チャンクオーバーラップを指定すると、コンテンツを重複させることができます。
実際にRAGで利用する際にはある程度の重複をさせたほうが前後の文脈を拾いやすいので、ある程度のオーバーラップをさせるのが良いらしいです。

const text = "吾輩は猫である。名前はまだない。どこで生れたか頓と見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。";
const splitter = new RecursiveCharacterTextSplitter({ 
    chunkSize: 20,    // チャンクの文字数(これを超えない限りは区切り文字があっても1つになるみたい)
    chunkOverlap: 10  // チャンクのオーバーラップ(こっちは効いてる)
});
const result = await splitter.createDocuments([text]);
console.log(result);

この長さでオーバーラップしてる意味はないけど、ちゃんとできてますね。

[
  Document {
    pageContent: '吾輩は猫である。名前はまだない。どこで生',
    metadata: { loc: [Object] }
  },
  Document {
    pageContent: 'はまだない。どこで生れたか頓と見当がつか',
    metadata: { loc: [Object] }
  },
  Document {
    pageContent: 'れたか頓と見当がつかぬ。何でも薄暗いじめ',
    metadata: { loc: [Object] }
  },
  Document {
    pageContent: 'ぬ。何でも薄暗いじめじめした所でニャーニ',
    metadata: { loc: [Object] }
  },
  Document {
    pageContent: 'じめした所でニャーニャー泣いていた事だけ',
    metadata: { loc: [Object] }
  },
  Document {
    pageContent: 'ャー泣いていた事だけは記憶している。',
    metadata: { loc: [Object] }
  }
]

自分で作る場合にはTextSplitterを継承します。例えばセパレータを受け取って、それぞれのドキュメントに語尾をつける場合はこうなります。

export interface MyTextSplitterParams extends TextSplitterParams {
    separator: string;
    suffix: string;
}

export class MyTextSplitter extends TextSplitter {

    readonly separator: string;
    readonly suffix: string;

    constructor(fields?: Partial<MyTextSplitterParams>) {
        super(fields);
        this.separator = fields?.separator ?? "";
        this.suffix = fields?.suffix ?? "";
    }

    splitText(text: string): Promise<string[]> {
        return Promise.resolve(
            text.split(this.separator)
                .filter(line => line)
                .map(line => line + this.suffix)
        );
    }
}

まぁ実用性はないですけどね。

const text = "吾輩は猫である。名前はまだない。どこで生れたか頓と見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。";
const splitter = new MyTextSplitter({
    separator: "。", // 区切り文字
    suffix: "にゃ"     // 区切り文字の後に付ける文字
});
const result = await splitter.createDocuments([text]);
console.log(result);

こうなります。

[
  Document { pageContent: '吾輩は猫であるにゃ', metadata: { loc: [Object] } },
  Document { pageContent: '名前はまだないにゃ', metadata: { loc: [Object] } },
  Document {
    pageContent: 'どこで生れたか頓と見当がつかぬにゃ',
    metadata: { loc: [Object] }
  },
  Document {
    pageContent: '何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶しているにゃ',
    metadata: { loc: [Object] }
  }
]

埋め込み(Embedding)

Embeddingを利用するとテキストをベクトルに変換することができます。これはざっくり言えばテキストを意味としての数値表現にする手法です。
例えばOpenAIの「text-embedding-ada-002」モデルでは1536次元のベクトルに変換されます。
最近リリースされた「text-embedding-3-large」では3072次元にできるらしいので、そのうち表現力はどんどん上がっていきそうですね。

const embeddings = new OpenAIEmbeddings();
const result = await embeddings.embedQuery("吾輩は猫である");
console.log(result);

人間が見てもその意味はさっぱりわかりません。

[
  -0.0017636258,  -0.0072244154, -0.0052609853,   -0.015052963,  -0.024114948,
    0.013240566,   -0.012416177,  -0.018337931,   -0.002806698,  -0.022755649,
  ... 略
]

ベクトルがどれくらい近しいかの計算にはコサイン類似度を利用することが多いので試してみます。
(コサイン類似度の計算式ってどんなだっけなと思いながら関数名とシグネチャ書いたらGitHub Copilotが全部書いてくれた)

function cosineSimilarity(vector1: number[], vector2: number[]) {
    const dotProduct = vector1.reduce((acc, value, index) => acc + value * vector2[index], 0);
    const magnitude1 = Math.sqrt(vector1.reduce((acc, value) => acc + value * value, 0));
    const magnitude2 = Math.sqrt(vector2.reduce((acc, value) => acc + value * value, 0));
    return dotProduct / (magnitude1 * magnitude2);
}

const embeddings = new OpenAIEmbeddings();

const result1 = await embeddings.embedQuery("吾輩は猫である");
const result2 = await embeddings.embedQuery("吾輩は犬である");
const result3 = await embeddings.embedQuery("クロネコヤマト");

const similarity1 = cosineSimilarity(result1, result2);
const similarity2 = cosineSimilarity(result1, result3);
const similarity3 = cosineSimilarity(result2, result3);

console.log(similarity1);
console.log(similarity2);
console.log(similarity3);

吾輩は猫である」と「吾輩は犬である」>「吾輩は猫である」と「クロネコヤマト」>「吾輩は犬である」と「クロネコヤマト」の順になったのでなんとなくそれっぽいです。

0.9502395478294273
0.8155650228855305
0.796804328626663

ベクターストア(VectorStore)

ChromaDBの記事で書いたのでベクターストア自体については特に書きませんが、LangChainではChromaDBを含めベクターストアを簡単に組み込めるようになっています。
shironeko.hateblo.jp

const embeddings = new OpenAIEmbeddings();
const chroma = new Chroma(embeddings, {
    url: process.env.CHROMADB_URL,
    collectionName: "sample"
});

const result = await chroma.similaritySearch("ネコカワイイ", 2);
console.log(result);

すでに入れてあったデータがDocumentとしてサクッと取れました。

[
  Document { pageContent: '猫がとてもかわいい', metadata: { source: 'sample' } },
  Document { pageContent: '犬もとてもかわいい', metadata: { source: 'sample' } }
]

データを追加するときはaddDocumentsを使います。

const embeddings = new OpenAIEmbeddings();
const chroma = new Chroma(embeddings, {
    url: process.env.CHROMADB_URL,
    collectionName: "sample"
});

// ドキュメント追加
const text = "吾輩は猫である。名前はまだない。どこで生れたか頓と見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。";
const splitter = new CharacterTextSplitter({ 
    separator: "。",
    chunkSize: 30,
    chunkOverlap: 10
});
const docs = await splitter.createDocuments([text]);
await chroma.addDocuments(docs);

const result = await chroma.similaritySearch("ネコカワイイ", 10);
console.log(result);

「ニャーニャー」はそんなに猫っぽくないんですかね。お寿司に負けてる…。

[
  Document { pageContent: '猫がとてもかわいい', metadata: { source: 'sample' } },
  Document { pageContent: '犬もとてもかわいい', metadata: { source: 'sample' } },
  Document {
    pageContent: '吾輩は猫である。名前はまだない。どこで生れたか頓と見当がつかぬ',
    metadata: { loc: [Object] }
  },
  Document { pageContent: 'お寿司が食べたい', metadata: { source: 'sample' } },
  Document {
    pageContent: '何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している',
    metadata: { loc: [Object] }
  },
  Document { pageContent: 'とっても眠い', metadata: { source: 'sample' } }
]

検索機(Retriever)

Retrieverはそのままレトリバーと書かれていることが多い気がするのですが、それ自体が馴染みがなくて「犬」?ってなってしまうと思ったのであえて検索機としましたが、適切かどうか怪しいところです。
(ちなみにゴールデンレトリバーとかのレトリバーと一緒です。獲物の回収が上手だからレトリバーっていうらしい)

VectorStoreはasRetrieverメソッドで簡単にRetrieverに変換することができます。
VectorStoreを利用して検索を行うためのクラスって感じですね。これ単体だとあんまり意味を感じないですが、Chainに組み込むにはRetrieverにする必要があります。

const embeddings = new OpenAIEmbeddings();
const chroma = new Chroma(embeddings, {
    url: process.env.CHROMADB_URL,
    collectionName: "sample"
});

const retriever = chroma.asRetriever(3);
const result = await retriever.getRelevantDocuments("ネコカワイイ");
console.log(result);

自分で作る場合にはBaseRetrieverを継承します。例えばWikipediaAPIを使って検索するRetrieverだとこうなります。
(実際には毎回アクセスするのではなくベクターストアとかに入れておいたほうがいいとは思います)

export interface MyRetrieverInput extends BaseRetrieverInput {}

export class MyRetriever extends BaseRetriever {

    lc_namespace: string[] = [];

    constructor(fields: MyRetrieverInput) {
        super(fields);
    }

    async _getRelevantDocuments(query: string): Promise<Document[]> {

        // Wikipediaにアクセスするための便利なクラスがあるので使う
        const wikipediaQuery = new WikipediaQueryRun({
            baseUrl: "https://ja.wikipedia.org/w/api.php",
            maxDocContentLength: 1024
        });
        const result = await wikipediaQuery.call(query);

        // 適当なサイズに分割
        const textSplitter = new TokenTextSplitter({
            chunkSize: 256,
            chunkOverlap: 0
        });

        return textSplitter.createDocuments([result]);
    }
}

で、こうですね。

const retriever = new MyRetriever({});
const result = await retriever.getRelevantDocuments("すみっコぐらし");
console.log(result);

ちゃんと取れていそうです。

[
  Document {
    pageContent: 'Page: すみっコぐらし\n' +
      'Summary: すみっコぐらしは、2012年に発表されたサンエックスのキャラクター。サブタイトルは「ここがおちつくんです」。作者は元サンエックス制作本部デ
ザイン室所属デザイナーで現在フリーイラストレーターのよこみぞゆり。ひとりで絵と文どちらも担当している。平仮名の「すみっこ」は場所、カタカナの「すみっコ
」はキャラクターで使い分けられている。誕生のきっかけは学生時代にノートの隅に描いた落書き。 \n' +
      '',
    metadata: { loc: [Object] }
  },
  Document {
    pageContent: '連商品の売り上げ額は年間約200億円(2019年時点)。愛らしさと親しみやすさに定評があり、幅広い世代に支持されている。\n' +
      '2019年度「日本キャラクター大賞」グランプリ受賞。\n' +
      '\n' +
      '\n' +
      '== 設定 ==\n' +
      'すみっコとは、すみっこがおちつく、すみっこにあつまってくるなかまたちの総称である。キャラクターの大半はコンプレックスを抱えるため少々ネガティブ 
だが、それとは対照的に物事の捉え方がポジティブなタイプ',
    metadata: { loc: [Object] }
  },
  ...略
]

長い

なんかまとめたらとても長くなってしまった。もしかしたら分割するかもしれないです。
一応これでRAGを作るための要素はさらっと一通り網羅できたのではないかと思います。
もちろんLangChainには便利な機能がいろいろあるのですが、だいたい後は組み合わせというか…。(Agent系以外だとまたちょっと違ってくる)

次回はここまでの要素を組み合わせて実際に簡単なRAGシステムを作ってみることにします。
それでは。