JavaScriptLangChain.jsTypeScript

LangChain.jsで「関連性の高い記事」を検索する

LangChain.jsを使用して、関連記事検索機能を実装する方法についてまとめました。具体的には、LangChain.jsを使用して関連性の高いデータを取得する方法や、WordPressの記事データを使用して関連記事を表示する方法などを解説しています。また、特定のキーワードやテキストに対してフィルタリングを行い、関連記事の抽出を行う方法も説明しています。これを実装することで、関連記事の表示や検索機能を実現することが可能です。

広告ここから
広告ここまで

OpenAIのGPT-3で遊ぶためにLangChain.jsを触っていたのですが、「どうも関連記事検索ができそうだ」と感じた部分があったので、ひとまずまとめてみました。

やろうとしていること

  • 特定のキーワードやテキストに対して、関連性の高いデータを表示する
  • データはREST APIなどで事前に取得し、LangChain.jsに判断をまかせる
  • すべての記事を対象にはせず、「最近の記事50件」などある程度フィルタする

ライブラリのインストール

まずはライブラリを追加します。

% yarn add langchain hnswlib-node

langchainと一緒にhnswlib-nodeも追加しましょう。

Langchain.jsのサンプルコードを動かす

まずはサンプルコードを動かしましょう。

元コード: https://github.com/hwchase17/langchainjs/blob/main/examples/src/indexes/vector_stores/hnswlib.ts

import { HNSWLib } from "langchain/vectorstores";
import { OpenAIEmbeddings } from "langchain/embeddings";

export const run = async () => {
  const vectorStore = await HNSWLib.fromTexts(
    ["Hello world", "Bye bye", "hello nice world"],
    [{ id: 2 }, { id: 1 }, { id: 3 }],
    new OpenAIEmbeddings()
  );

  const resultOne = await vectorStore.similaritySearch("hello world", 1);
  console.log(resultOne);
};

このスクリプトを動かすと、hello worldと関連の高いデータを1件取得できます。

[ Document { pageContent: 'Hello world', metadata: { id: 2 } } ]

OpenAIのAPIも使わずに判定していますので、APIキーなどは不要です。

WordPressの記事データを使ってみる

サンプルコードのおかげで、渡すデータのフォーマットが大体把握できました。

早速WP APIをfetchで呼び出してデータ元データにしてみましょう。


import fetch from "node-fetch";
import { HNSWLib } from "langchain/vectorstores";
import { OpenAIEmbeddings } from "langchain/embeddings";

export const run = async () => {
  const result = await fetch(`https://wp-api.example.com/wp-json/wp/v2/posts?per_page=50`)
  const resposne = await result.json()
  const vectorStore = await HNSWLib.fromTexts(
    (resposne as any[]).map((post) => post.title.rendered),
    (resposne as any[]).map((post) => ({id: post.id})),
    new OpenAIEmbeddings()
  );

  const resultOne = await vectorStore.similaritySearch("ReactからAstroへの移行", 1);
  console.log(resultOne);
};
run();

動かすの優先でas anyを使っていますが、実際に使う場合は型定義をつけることをお勧めします。

これを実行すると、ReactからAstroへの移行と関連性の高い記事を、取得した50記事の中から1つ選んでくれます。

[
  Document {
    pageContent: 'ReactコンポーネントをAstroコンポーネントに書き換える際の、クラス名の取り扱いについて',
    metadata: { id: 11960 }
  }
]

「今表示している記事」を除外する

WP APIで取得した記事一覧の中には、「今見ている記事」も含まれることがあります。

その場合は、filterを使って結果から除外するとよいでしょう。

  const results = await vectorStore.similaritySearch("ReactからAstroへの移行", 5);
  const searchTargets = (results as any[]).filter(post => post.id === resultOne[0].metadata.id)

フリーワード検索として利用する

検索した結果をそのままページ表示などに使いたい場合、例えばmapを使ってIDから記事データを取得すると良いかもしれません。

export const run = async () => {
  const result = await fetch(`https://wp-api.example/wp-json/wp/v2/posts?per_page=50`)
  const resposne = await result.json()
  const vectorStore = await HNSWLib.fromTexts(
    (resposne as any[]).map((post) => post.title.rendered),
    (resposne as any[]).map((post) => ({id: post.id})),
    new OpenAIEmbeddings()
  );

  const searchResult = await vectorStore.similaritySearch("AngularとReact", 3);
  return searchResult.map(item => {
    const post = (resposne as any[]).find(post => post.id === item.metadata.id)
    return post
  })
};
run().then(posts => console.log(posts.map(post => ({id: post.id, title: post.title.rendered}))))

上のスクリプトは、このような結果がでます。

[
  { id: 11371, title: 'Angular + nxrxにRedux Toolkitを追加してみた覚書' },
  { id: 11804, title: 'Nxで、Ionic(React)アプリを開発する方法' },
  { id: 11378, title: 'Ionic Angularで汎用的に利用したいカスタムComponentを作る方法の覚書' }
]

関連記事表示機能として使う

もしくは表示中の記事のタイトルを使って、関連記事を表示させることも可能そうです。

const POST_TITLE = 'ReactコンポーネントをAstroコンポーネントに書き換える際の、クラス名の取り扱いについて'
export const run = async () => {
  const result = await fetch(`https://wp-api.example/wp-json/wp/v2/posts?per_page=50`)
  const resposne = await result.json()
  const vectorStore = await HNSWLib.fromTexts(
    (resposne as any[]).map((post) => post.title.rendered),
    (resposne as any[]).map((post) => ({id: post.id})),
    new OpenAIEmbeddings()
  );

  const searchResult = await vectorStore.similaritySearch(POST_TITLE, 4);
  return searchResult.map(item => {
    const post = (resposne as any[]).find(post => post.id === item.metadata.id)
    return post
  }).filter(post => post.title.rendered !== POST_TITLE)
};
run().then(posts => console.log(posts.map(post => ({id: post.id, title: post.title.rendered}))))

この場合は、表示中の記事が含まれないようにfilterなどで除外してやりましょう。

ブックマークや限定記事(予定)など

WP Kyotoサポーター募集中

WordPressやフロントエンドアプリのホスティング、Algolia・AWSなどのサービス利用料を支援する「WP Kyotoサポーター」を募集しています。
月額または年額の有料プランを契約すると、ブックマーク機能などのサポーター限定機能がご利用いただけます。

14日間のトライアルも用意しておりますので、「このサイトよく見るな」という方はぜひご検討ください。

広告ここから
広告ここまで

Related Category posts