海外からのアクセス:www.kdjingpai.com
Ctrl + D このサイトをブックマークする

ChatOllamaノート|生産性とRedisベースの文書データベースのための高度なRAGの実装

 

ChatOllama LLMをベースにしたオープンソースのチャットボットです。ChatOllamaの詳細については、以下のリンクをクリックしてください。

 

ChatOllama|Ollamaベースの100%ローカルRAGアプリケーション

 

ChatOllama 当初の目標は、100%のネイティブRAGアプリケーションをユーザーに提供することでした。

 

その成長とともに、より多くのユーザーから貴重な要望が寄せられるようになりました。現在、`ChatOllama`は以下のような複数の言語モデルをサポートしています:

 

ChatOllamaで、ユーザーは次のことができます:

  • Ollamaモデルの管理(プル/削除)
  • ナレッジベースの管理(作成/削除)
  • LLMとの自由な対話
  • 個人の知識ベースの管理
  • 個人的な知識ベースを通じてLLMとコミュニケーションを図る

 

今回は、本番用の高度なRAGを実現する方法を見ていこうと思う。RAGの基本的なテクニックと高度なテクニックを学びましたが、RAGをプロダクションに導入するためには、まだまだ気をつけなければならないことがたくさんあります。ChatOllamaで行われてきた作業と、RAGを本番に持ち込むために行われてきた準備を共有します。

 

ChatOllama 笔记 | 实现高级RAG的生产化和基于Redis的文档数据库-1

ChatOllama|RAGコンストラクテッド・ラン

 

ChatOllamaプラットフォームではLangChain.jsを使ってRAGプロセスを操作します。ChatOllamaの知識ベースでは、オリジナルのRAGを超えた親ドキュメント検索が使用されています。そのアーキテクチャと実装の詳細に深く潜ってみましょう。私たちは、あなたがそれを啓発見つけることを願っています。

 

 

親ドキュメント検索ユーティリティ

 

親ドキュメント・リトリーバーを実際の製品で機能させるためには、その核となる原理を理解し、生産目的に適したストレージ・エレメントを選択する必要がある。

 

親ドキュメント検索者の基本原則

検索が必要な文書を分割する場合、しばしば相反する要件が発生する:

  • 埋め込まれたコンテンツがその意味を最も正確に表すことができるように、ドキュメントはできるだけ小さくすることを望むかもしれません。コンテンツが長すぎると、埋め込まれた情報が意味を失ってしまうかもしれません。
  • また、各段落に完全なフルテキスト環境が確保できるよう、十分な長さの文書も必要だ。

 

Powered by LangChain.jsParentDocumentRetrieverこのバランスは、文書をチャンクに分割することで達成される。検索のたびに、チャンクを抽出し、各チャンクに対応する親ドキュメントを検索し、より広い範囲のドキュメントを返します。一般的に、親文書はdoc idによってチャンクにリンクされています。この仕組みについては後で詳しく説明します。

ここではparent documentソース文書の小片を指す。

 

ビルド

親ドキュメント・リトリーバの全体的なアーキテクチャ図を見てみよう。

各チャンクは処理され、ベクトルデータに変換され、ベクトルデータベースに格納される。 これらのチャンクを取り出すプロセスは、オリジナルのRAGセットアップで行われたものと同じである。

親文書(block-1、block-2、......block-m)は小さな部分に分割される。 ここでは2つの異なるテキスト分割方法が使用されていることに留意されたい。親文書には大きな分割方法、小さなブロックには小さな分割方法である。各親文書には文書IDが与えられ、このIDは対応するチャンクのメタデータとして記録される。 これにより、各チャンクは、メタデータに格納された文書IDを使って、対応する親文書を見つけることができる。

親文書を検索するプロセスはこれと同じではない。類似性検索の代わりに、文書IDが提供され、対応する親文書を検索する。この場合、キー・バリュー・ストレージ・システムで十分である。

 

ChatOllama 笔记 | 实现高级RAG的生产化和基于Redis的文档数据库-2

親ドキュメントレトリバー

 

保管セクション

保存すべきデータには2種類ある:

  • 小規模ベクトルデータ
  • 文書IDを含む親文書の文字列データ

 

主要なベクターデータベースはすべて利用できる。ChatOllama`のうち、私はChromaを選んだ。

親ドキュメント・データには、最もポピュラーでスケーラビリティの高いキー・バリュー・ストレージ・システムであるRedisを選んだ。

 

LangChain.jsに欠けているもの

LangChain.jsは、バイトストレージとドキュメントストレージを統合する方法を数多く提供している:

 

ストレージ|🦜️🔗 ラングチェーン

データをキーと値のペアとして格納することは、高速かつ効率的であり、LLMアプリケーションの強力なツールである。ベースリポジトリ...

js.langchain.com

 

IORedis`のサポート:

 

イオレディス|🦜️🔗 ラングチェーン

この例では、RedisByteStore ベースリポジトリの統合を使用して、チャット履歴用のストレージを設定する方法を示します。

js.langchain.com

 

RedisByteStoreに欠けている部分はcollectionメカニズム

 

異なる知識ベースからのドキュメントを処理する場合、各知識ベースはcollectionコレクションはコレクションの形で整理され、同じライブラリ内のドキュメントはベクターデータに変換され、Chromaのようなベクターデータベースのいずれかに保存される。collection集会で

 

知識ベースを削除したいとします。確かにChromaデータベースの`コレクション`コレクションを削除することはできます。しかし、ナレッジベースのディメンションに従ってドキュメントストアをクリーンアップするにはどうすればいいでしょうか?RedisByteStoreのようなコンポーネントは`コレクション`機能をサポートしていないので、自分で実装する必要がありました。

 

ChatOllama RedisDocStore

Redis には `collection` と呼ばれる組み込みの機能はない。開発者は通常、プレフィックスキーを使用して `collection` 機能を実装します。次の図は、異なるコレクションを識別するためにプレフィックスを使用する方法を示しています:

 

ChatOllama 笔记 | 实现高级RAG的生产化和基于Redis的文档数据库-3

プレフィックスを使ったRedisのキー表現

 

では、どのように実装されたのかを見てみよう。Redis背景の文書保存機能の

 

キーメッセージ

  • 各 `RedisDocstore` は `namespace` パラメータで初期化される。(名前空間とコレクションの命名は現時点では標準化されていないかもしれない)。
  • キーは、get操作とset操作の両方で、最初に`名前空間`が処理される。

 

import { Document } from “@langchain/core/documents”;
import { BaseStoreInterface } from “@langchain/core/stores”;
import { Redis } from “ioredis”;
import { createRedisClient } from “@/server/store/redis”;

export class RedisDocstore implements BaseStoreInterface<string, Document>
{
_namespace: string;
_client: Redis;

constructor(namespace: string) {
this._namespace = namespace;
this._client = createRedisClient();
}

serializeDocument(doc: Document): string {
return JSON.stringify(doc);
}

deserializeDocument(jsonString: string): Document {
const obj = JSON.parse(jsonString);
return new Document(obj);
}

getNamespacedKey(key: string): string {
return `${this._namespace}:${key}`;
}

getKeys(): Promise<string[]> {
return new Promise((resolve, reject) => {
const stream = this._client.scanStream({ match: this._namespace + ‘*’ });

const keys: string[] = [];
stream.on(‘data’, (resultKeys) => {
keys.push(…resultKeys);
});

stream.on(‘end’, () => {
resolve(keys);
});

stream.on(‘error’, (err) => {
reject(err);
});
});
}

addText(key: string, value: string) {
this._client.set(this.getNamespacedKey(key), value);
}

async search(search: string): Promise<Document> {
const result = await this._client.get(this.getNamespacedKey(search));
if (!result) {
throw new Error(`ID ${search} not found.`);
} else {
const document = this.deserializeDocument(result);
return document;
}
}

/**
* Adds new documents to the store.
* @param texts An object where the keys are document IDs and the values are the documents themselves.
* @returns Void
*/
async add(texts: Record<string, Document>): Promise<void> {
for (const [key, value] of Object.entries(texts)) {
console.log(`Adding ${key} to the store: ${this.serializeDocument(value)}`);
}

const keys = […await this.getKeys()];
const overlapping = Object.keys(texts).filter((x) => keys.includes(x));

if (overlapping.length > 0) {
throw new Error(`Tried to add ids that already exist: ${overlapping}`);
}

for (const [key, value] of Object.entries(texts)) {
this.addText(key, this.serializeDocument(value));
}
}

async mget(keys: string[]): Promise<Document[]> {
return Promise.all(keys.map((key) => {
const document = this.search(key);
return document;
}));
}

async mset(keyValuePairs: [string, Document][]): Promise<void> {
await Promise.all(
keyValuePairs.map(([key, value]) => this.add({ [key]: value }))
);
}

async mdelete(_keys: string[]): Promise<void> {
throw new Error(“Not implemented.”);
}

// eslint-disable-next-line require-yield
async *yieldKeys(_prefix?: string): AsyncGenerator<string> {
throw new Error(“Not implemented”);
}

async deleteAll(): Promise<void> {
return new Promise((resolve, reject) => {
let cursor = ‘0’;

const scanCallback = (err, result) => {
if (err) {
reject(err);
return;
}

const [nextCursor, keys] = result;

// Delete keys matching the prefix
keys.forEach((key) => {
this._client.del(key);
});

// If the cursor is ‘0’, we’ve iterated through all keys
if (nextCursor === ‘0’) {
resolve();
} else {
// Continue scanning with the next cursor
this._client.scan(nextCursor, ‘MATCH’, `${this._namespace}:*`, scanCallback);
}
};

// Start the initial SCAN operation
this._client.scan(cursor, ‘MATCH’, `${this._namespace}:*`, scanCallback);
});
}
}

 

このコンポーネントは ParentDocumentRetriever とシームレスに使用できます:

retriever = new ParentDocumentRetriever({
vectorstore: chromaClient,
docstore: new RedisDocstore(collectionName),
parentSplitter: new RecursiveCharacterTextSplitter({
chunkOverlap: 200,
chunkSize: 1000,
}),
childSplitter: new RecursiveCharacterTextSplitter({
chunkOverlap: 50,
chunkSize: 200,
}),
childK: 20,
parentK: 5,
});

私たちは現在、`Chroma`と`Redis`とともに、高度なRAG、Parent Document Retrieverのためのスケーラブルなストレージソリューションを手に入れました。

おすすめ

AIツールが見つからない?こちらをお試しください!

キーワードを入力する アクセシビリティこのサイトのAIツールセクションは、このサイトにあるすべてのAIツールを素早く簡単に見つける方法です。

トップに戻る